Logo Tech Writers

Tech writers

Esse é o nosso blog para apaixonados por tecnologia! Aqui, softplayers e outros especialistas compartilham conhecimentos fundamentais para o desenvolvimento dessa comunidade.

SEJA UM TECH WRITER
RFC 9745 e seu impacto na governança de APIs
Tech Writers Junho 10, 2025

RFC 9745 e seu impacto na governança de APIs

A Internet Engineering Task Force (IETF) - organização internacional aberta que desenvolve e promove padrões técnicos para a internet, como os protocolos TCP/IP, HTTP e DNS - acaba de lançar a RFC 9745, que define uma forma padronizada de se informar a depreciação de recursos no contexto do HTTP, o que é especialmente relevante para APIs baseadas neste protocolo. Em resumo, a partir de agora, servidores podem informar seus clientes sobre o status de depreciação de determinados recursos utilizando o cabeçalho de resposta Deprecation, cujo valor é uma data, no passado ou futuro, indicando que o recurso já foi ou ainda será depreciado. Adicionalmente, é possível utilizar o cabeçalho link para apontar uma documentação e, também, o Sunset, trazendo a data na qual o recurso se tornará indisponível. Neste artigo iremos avaliar a viabilidade de se aplicar este padrão no mundo real. Utilizando as melhores práticas no desenvolvimento de APIs, partiremos de arquivos de definição que seguem a OpenAPI Specification e terminaremos no API gateway KrakenD. Impactos Como dito, este novo padrão é especialmente importante para as web APIs, ou seja, para as APIs que aderem ao protocolo HTTP, como as famosas REST. Neste contexto, a RFC oferece meios de se levar as políticas de depreciação — outrora restritas à documentação ou ao design time — ao tempo de execução. Portanto, a novidade tem o potencial de seriamente mitigar as quebras de integração, possibilitando aos desenvolvedores realizarem as adaptações necessárias com uma antecedência confortável. Aliás, vale lembrar que estamos entrando na era da A.I. (com seus agentes, servidores MCP e etc.), o que só faz aumentar o impacto deste novo padrão, já que eles podem aprender e se adaptar sozinhos diante da sinalização de depreciação. No contexto de governança, a RFC também torna possível aos fornecedores de gateways de API (como Kong, Tyk, KrakenD, Traefik, APISix e etc.) considerarem o novo padrão durante os processos automatizados de deploy de APIs, sobretudo quando pensamos em APIOps baseado em OpenAPI specification. Vejamos. A especificação OpenAPI prevê a indicação de depreciação de operações através do campo deprecated. Com esta nova RFC, é simplesmente natural pensarmos em linkar as coisas, ou seja, fazer com que a indicação de depreciação presente nos arquivos de definição encontre correspondência na configuração dos gateways, que, uma vez em execução, passem a injetar o novo cabeçalho de resposta nas operações apropriadas. Esta melhoria levaria a governança ao próximo nível de qualidade! Provando o conceito Utilizaremos o arquivo de definição aderentes à OpenAPI Specification (OAS) para descrever nossa API, construiremos um parser em Go utilizando a libopenapi, contaremos com o KrakenD como API gateway e um HttpBin como backend. Todos os detalhes do projeto podem ser encontrados neste repositório. Então, vou destacar apenas os pontos principais: O arquivo de definição (openapi.yaml) paths: (...) /users/{userId}: (...) delete: (...) deprecated: true Observe que a operação de deleção de usuário conta com o campo padrão da OAS deprecated com o valor true. Bem, é fácil perceber que estamos diante de uma incompatibilidade de impedância quando tentamos fazer esse booleano interagir com os novos cabeçalhos previstos na RFC 9745, já que estes são muito mais ricos em informação do que aquele. Por razões como esta, a OAS possui extensões, que, no nosso caso, serão utilizadas para descrever as propriedades esperadas pela RFC da seguinte forma: paths: (...) /users/{userId}: (...) delete: (...) deprecated: true x-deprecated-at: "2025-06-30T23:59:59Z" x-deprecated-sunset: "2026-01-01T00:00:00Z" x-deprecated-link: https://api.example.com/deprecation-policy O Parser A função do parser é ler e interpretar o arquivo de definição openapi.yaml, extrair as informações relevantes para o gateway, e criar o arquivo operations.json, que será embarcado na imagem do KrakenD e consumido durante a sua inicialização, numa abordagem denominada configuração flexível. Este é o resultado do operations.json: { "list": [ { "path": "/users", "method": "get", "backend": { "path": "/users", "host": "http://backend:8888" } }, { "path": "/users", "method": "post", "backend": { "path": "/users", "host": "http://backend:8888" } }, { "path": "/users/{userId}", "method": "get", "backend": { "path": "/users/{userId}", "host": "http://backend:8888" } }, { "path": "/users/{userId}", "method": "delete", "deprecated": { "at": "@1751327999", "link": "https://api.example.com/deprecation-policy", "sunset": "Thu, 01 Jan 2026 00:00:00 UTC" }, "backend": { "path": "/users/{userId}", "host": "http://backend:8888" } } ] } Observe que o parser projetou os elementos estendidos da OAS no arquivo de configuração do KrakenD, inclusive fazendo as devidas conversões de valores, da seguinte forma: OAS KrakenD x-deprecated-at: deprecated.at: x-deprecated-link: deprecated.link: x-deprecated-sunset: deprecated.sunset: O plugin Agora que a configuração do gateway foi devidamente gerada a partir do arquivo de definição, nosso plugin personalizado entra em cena. A sua função é identificar as operações de API depreciadas e inserir os cabeçalhos da RFC 9745 com os valores adequados. Mais detalhes podem ser encontrados no repositório do artigo. Mas, uma vez que o plugin foi embarcado no KrakenD, temos os seguintes resultados: GET /users/1 DELETE /users/1 Observe que apenas a segunda operação estava depreciada (vide operations.json) e o gateway adicionou corretamente os cabeçalhos na resposta. Conclusões O experimento mostrou a viabilidade do conceito, ou seja, que é possível levar as políticas de depreciação para além da definição e documentação, sendo facilmente comunicadas em tempo de execução. Desta forma, os sistemas podem adotar ações automatizadas para comunicar a obsolescência aos interessados e reduzir significativamente as chances de falhas nas integrações. Embora as extensões da OpenAPI Specification tenham tornado isso possível diante da insuficiência do booleano deprecated, imagino que a OpenAPI Initiative deve incluir uma melhoria nas próximas versões. Sobretudo quando penso que Eric Wilde, co-autor desta RFC, é bem atuante no mundo das APIs. Aos leitores que chegaram até aqui, meu muito obrigado. Espero que estas poucas palavras lhes tenham acrescentado algo e feito o seu tempo valer a pena. Referências RFC 9745: https://www.rfc-editor.org/rfc/rfc9745 OpenAPI Specification: https://spec.openapis.org/oas/latest.html Incompatibilidade de Impedância: https://devblogs.microsoft.com/oldnewthing/20180123-00/?p=97865 Repositório: https://github.com/MichelFortes/articles-RFC9745 HttpBin: https://hub.docker.com/r/michelfortes/httpbin KrakenD – flexible configuration: https://www.krakend.io/docs/configuration/flexible-config PB33F - libopenapi: https://pb33f.io/libopenapi/

Embeddings: o que são e suas aplicações
Tech Writers Maio 27, 2025

Embeddings: o que são e suas aplicações

Sabemos que com o surgimento de diversas tecnologias, há um grande aumento do número de termos que ouvimos falar, embeddings é um deles, mas o que são?Embeddings, que em inglês significa "incorporar", é um termo utilizado em IA e Processamento de Linguagem Natural (PLN). Refere-se ao processo de "incorporar" ou "embutir" informações complexas (como palavras, frases ou documentos) em um espaço vetorial. Isso significa que dados que seriam difíceis de processar diretamente são transformados em uma forma numérica (vetores), que os modelos de Machine Learning podem entender e usar para tarefas como classificação e análise semântica. Quando combinados com bancos de dados vetoriais, possibilitam que sistemas analisem grandes volumes de dados não estruturados. Isso permite a extração de informações relevantes e consultas complexas de forma rápida e eficaz. Essa técnica de transformação de dados é essencial na construção de soluções escaláveis, pois a representação vetorial facilita a busca e recuperação de informações além de comprimir suas informações e ainda assim manter a relação com o seu conteúdo original. Como funciona Sabemos que Embeddings são vetores para entendimento de máquina baseados em textos, fases, documentos. Mas como transformamos essas informações em vetores?Os vetores são formados a partir da utilização de modelos de IA treinados para identificar contextos, classificando-os com base na aproximação do contexto em números, que normalmente variam de -1 a 1. O valor 1 indica a maior proximidade, com milhares de parâmetros de comparação. Esses modelos são geralmente treinados com grandes volumes de texto, identificando padrões de concorrência entre palavras que aparecem frequentemente em contextos semelhantes, como "gato" e "animal". Durante o treinamento, o modelo aprende a mapear essas palavras para vetores numéricos em um espaço multidimensional, de forma que palavras com significados relacionados ou contextos similares fiquem posicionadas mais próximas entre si nesse espaço vetorial. O objetivo é fazer com que palavras ou frases com significados semelhantes fiquem mais próximas no "espaço" dos vetores. Por exemplo, "gato" e "cachorro" devem ser representados por vetores próximos, enquanto "gato" e "carro" estarão mais distantes. Exemplo de embedding | Imagem: https://arize.com/blog-course/embeddings-meaning-examples-and-how-to-compute/ De que forma é calculada a semelhança entre dois vetores, comparando, por exemplo, um texto com diversos vetores do modelo treinado?Matematicamente se utiliza normalmente a técnica de similaridade por cosseno para realizar a comparação entre dois vetores A similaridade do cosseno fornece um valor no intervalo [-1,1], tendo 1 como o valor de contexto mais próximo e -1 o mais distante [1] Equação de similaridade por cosseno | Imagem: Wikipedia Dois vetores com 98% de similaridade com base no cosseno do ângulo entre os vetores | Imagem: Richmond Alake Embeddings, na prática Análise de PDF com QA (Question Answering): Embeddings são usados em sistemas de análise de documentos, como PDFs, para realizar tarefas de Pergunta e Resposta (QA). Empresas que lidam com grandes volumes de documentos, como contratos ou relatórios, podem utilizar embeddings para localizar automaticamente trechos relevantes em um texto. Por exemplo, ao analisar um contrato em PDF, os embeddings permitem mapear semanticamente o conteúdo e identificar passagens relacionadas a perguntas como "Qual é o prazo de validade deste contrato?" ou "Quais são as obrigações de pagamento do cliente?". Em seguida, um modelo de IA generativa pode utilizar esses trechos para interpretar o contexto e gerar respostas em linguagem natural com maior precisão. Recomendação de Produtos (E-commerce): Plataformas como Amazon e Netflix utilizam embeddings para recomendar produtos ou filmes baseados nas preferências e comportamentos passados dos usuários. Por exemplo, ao recomendar filmes, embeddings são usados para capturar o estilo, gênero e características dos filmes que o usuário assistiu, sugerindo novos conteúdos com base na similaridade vetorial. Análise de Sentimentos (Atendimento ao Cliente): Empresas utilizam embeddings para analisar sentimentos em feedbacks ou mensagens de clientes. Por exemplo, ao analisar um conjunto de comentários em redes sociais ou e-mails de clientes, embeddings ajudam a identificar automaticamente se o sentimento é positivo, negativo ou neutro, permitindo uma resposta rápida e apropriada. Conclusão Embeddings têm se mostrado uma ferramenta poderosa e crescente em diversas indústrias, transformando a forma como interagimos com dados não estruturados. Sua capacidade de representar informações complexas de maneira numérica tem levado a melhorias em sistemas de análise de documentos, recomendações e até no atendimento ao cliente. Sendo uma tecnologia em constante evolução, é esperado que, com o tempo, ela seja cada vez mais integrada em soluções inteligentes e escaláveis. Além disso, com a tendência de redução dos custos computacionais e o avanço das infraestruturas de processamento e armazenamento, torna-se cada vez mais viável escalar essas soluções com eficiência e baixo custo. Referências https://builtin.com/machine-learning/cosine-similarity#:~:text=Cosine%20similarity%20is%20a%20measurement,within%20an%20inner%20product%20space https://arize.com/blog-course/embeddings-meaning-examples-and-how-to-compute

Como o Karpenter otimizou a gestão da nossa infraestrutura EKS na AWS
Tech Writers Maio 13, 2025

Como o Karpenter otimizou a gestão da nossa infraestrutura EKS na AWS

Empresas enfrentam desafios diários na gestão de infraestrutura Kubernetes, especialmente para manter eficiência e reduzir custos. Aqui na Softplan, descobrimos uma solução que transforma a maneira de gerenciar nossos clusters EKS na AWS: o Karpenter. Desafios na gestão de instâncias Antes de falar de Karpenter é preciso dar alguns passos atrás e explicar um pouco do que se trata um auto escalonamento de nodes. Suponha que temos nosso cluster com algumas máquinas (instâncias) disponíveis executando nossos workloads. O que acontece se por acaso houver um pico de uso em nossas aplicações e seja necessário subir mais instâncias (réplicas) de nossos pods? Sem um autoscaling precisaríamos provisionar um node, orientá-lo a juntar-se ao nosso cluster para aí sim nossos pods estarem aptos a serem iniciados nessa nova instância. Lembrando que o provisionamento de uma instância não é instantâneo, há todo um bootstrapping da máquina, configurações de rede e muitas outras coisas antes dela ficar totalmente disponível. Certo, falamos sobre pico de usuários em nossas aplicações, mas e quando houver ociosidade? Queremos mesmo deixar esses nodes em pé com poder computacional subutilizado? Para resolver essa e outras questões, entra em cena o conceito de auto scalers. Auto Scalers As implementações de auto scalers são responsáveis basicamente pelo provisionamento e consolidação de nodes. Aqui estamos falando de escalonamento horizontal, ou seja, adicionando mais máquinas em nosso cluster. Há diversas implementações de node autoscaling, mas neste artigo o foco será na implementação da AWS e por que decidimos migrar para uma outra solução. Abaixo uma figura exemplificando o funcionamento do node autoscaling: Figura 01: autoscaling AWS - Auto Scaling Groups Ao definir um grupo de escalonamento na AWS precisamos definir diversas propriedades, como o número mínimo/máximo de instâncias de nodes permitidas para este grupo, recursos utilizados, tipo de disco, configurações de rede (subnets, etc) e muitos outros detalhes. Por exemplo, para um determinado tipo de aplicação que utilize mais CPU vamos configurar um grupo que contenha tipos de instância com mais CPU do que memória. No fim possivelmente teremos alguns grupos distintos para certos tipos de aplicações. Juntando as peças – Cluster Auto Scaler Para que meu cluster consiga “conversar” com meu cloud provider (neste exemplo AWS), precisamos de um componente chamado Cluster Auto Scaler, ou CAS.Este componente foi criado pela própria comunidade que mantém o Kubernetes, e está disponível aqui. Uma configuração padrão do CAS pode ser vista abaixo, utilizando o helm para instalação: nameOverride: cluster-autoscaler awsRegion: us-east-1 autoDiscovery: clusterName: meu-cluster image: repository: registry.k8s.io/autoscaling/cluster-autoscaler tag: v1.30.1 tolerations: - key: infra operator: Exists effect: NoSchedule nodeSelector: environment: "infra" rbac: create: true serviceAccount: name: cluster-autoscaler annotations: eks.amazonaws.com/role-arn: "role-aws" extraArgs: v: 1 stderrthreshold: info Com isso configurado e instalado e nossos autoscaling groups criados acabamos de habilitar o gerenciamento automático de nossos nodes! Por que decidimos migrar para o Karpenter Nosso caso de uso aqui na Projuris é o seguinte: temos um cluster de desenvolvimento e outro de produção. Depois da migração para o Gitlab SaaS tínhamos um desafio de como provisionar os runners para a execução de nossas pipelines. Ficou decidido que usaríamos o cluster de desenvolvimento para provisionamento desses runners. Na “primeira versão” optamos pelo cluster auto scaler por ser uma configuração mais simples e que já atendia nosso setup em produção. Mas aí começamos a enfrentar alguns problemas com esta escolha: Tempo de provisionamento: ao iniciar uma pipeline o tempo de provisionamento da máquina era um pouco lento. O grande ponto é que o cluster auto scaler paga um “pedágio” no cloud provider para provisionamento de um novo node. Dificuldade na configuração de grupos: como temos alguns “perfis” de pipeline essa gestão ficou um pouco complicada, porque para cada novo perfil um novo node group precisa ser criado. Custo: para mitigar o problema de lentidão no startup de um novo node tínhamos um perfil de máquina “online” que ficava o tempo todo de pé, mesmo sem executar nenhuma pipeline. O que é o Karpenter? É uma solução de cluster autoscaling criada pela AWS, que promete o provisionamento e consolidação de nodes sempre com o menor custo possível. Ele é inteligente o suficiente para saber que por exemplo, ao comprar uma máquina na AWS do tipo on-demand, dependendo da situação, é mais em conta do que se fosse uma máquina spot. E essa é apenas uma das características dessa ferramenta. O Karpenter também trabalha com a ideia de “grupos” de máquinas (que no mundo do Karpenter chamamos de NodePools), só que a diferença é que fazemos isso através de CRDs (custom resource definitions) do próprio Karpenter, ou seja, temos manifestos dentro de nosso cluster com todas essas configurações, eliminando a necessidade de qualquer node group criado na AWS. Exemplo de um NodePool no Karpenter: apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: karpenter-gitlab-runner-small-online spec: template: metadata: labels: workload: gitlab-runners environment: karpenter-nodes-gitlab-runner-build-small-online spec: requirements: - key: "karpenter.sh/capacity-type" operator: In values: ["spot", “on-demand”] - key: "node.kubernetes.io/instance-type" operator: In values: ["m5d.large", "m5n.large", "m6id.large", "m6in.large"] nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: my-node-class taints: - key: "gitlab-runner-karpenter" value: "true" effect: NoSchedule expireAfter: Never disruption: consolidationPolicy: WhenEmpty consolidateAfter: 5m budgets: - nodes: "20%" limits: cpu: "500" memory: 500Gi Além do NodePool precisamos criar um NodeClass para definir detalhes específicos de instâncias AWS: apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: my-node-class spec: amiFamily: AL2 role: "aws-role" tags: Name: nodes-k8s-nodes-gitlab-runner-small-online subnetSelectorTerms: - tags: karpenter.sh/subnet: "my-subnet" securityGroupSelectorTerms: - id: "sg-123" - id: "sg-456" - id: "sg-789" amiSelectorTerms: - id: "imagem ami" kubelet: clusterDNS: ["111.222.333.44"] blockDeviceMappings: - deviceName: /dev/xvda ebs: volumeSize: 40Gi volumeType: gp3 encrypted: true OBS: perceba que o nome “my-node-class” precisa bater com o node class configurado no node pool. Como o Karpenter nos ajudou a superar os desafios apresentados? Tempo de provisionamento: como o Karpenter conversa diretamente com as APIs do cloud provider não é necessário pagar o pedágio do cluster auto scaler. Tínhamos muitos problemas de timeout no provisionamento de novos nodes, após a troca pelo Karpenter esse problema simplesmente desapareceu justamente porque o provisionamento é mais eficiente. Dificuldade na configuração de grupos: com a solução de NodePools e NodeClass do Karpenter essa configuração ficou trivial, e o mais importante, versionada em nosso controle de versões no Gitlab. Ou seja, precisa incluir um perfil de máquina novo no NodePool? Sem problemas, basta um commit e o Karpenter já irá considerar isso nos novos provisionamentos. Custo: Conseguimos utilizar a utilização de máquinas, pois agora runners com características semelhantes são alocados em nodes que suportem os requisitos de memória e CPU exigidos. Ou seja, estamos realmente usando todo o poder computacional que aquele node proporciona. Isso vale também para a consolidação de nodes. Com o cluster auto scaler haviam scripts complexos para fazer o drain dos nodes antes da consolidação. Com o Karpenter isso é configurado no NodePool de maneira muito simplificada. Um ótimo argumento para a gestão que justifique o investimento nesse tipo de mudança é custo. Abaixo temos um comparativo do custo utilizando o Cluster AutoScaler e o Karpenter em Janeiro/25, onde conseguimos uma economia de 16% no total: Figura 02: Período de 01/01 à 15/01 com ClusterAutoScaler Figura 03: Período de 16/01 à 31/01 com o Karpenter Considerações finais A migração para o Karpenter foi uma escolha acertada. Conseguimos simplificar a gestão de nossos nodes com diferentes perfis de forma bastante simplificada. Ainda há espaço para algumas melhorias, como por exemplo a utilização de um único NodePool para simplificar ainda mais, e deixar que os runners configurem labels específicas para o perfil de máquina que deve ser provisionado para o runner (mais em https://kubernetes.io/docs/reference/labels-annotations-taints/). Referências Karpenter (doc oficial): https://karpenter.sh/ Node Auto Scaling (doc oficial k8s): https://kubernetes.io/docs/concepts/cluster-administration/node-autoscaling/

A força da colaboração e o cliente como protagonista: impactos da evolução de produtos no Grupo Softplan
Tech Writers Abril 29, 2025

A força da colaboração e o cliente como protagonista: impactos da evolução de produtos no Grupo Softplan

No Grupo Softplan, a evolução dos produtos é um esforço contínuo que envolve a colaboração entre times e um compromisso profundo com o cliente. Em minha função como Product Growth, troco constantemente ideias com outras equipes e recebo feedbacks valiosos dos clientes, seja pela análise de como utilizam o produto ou através de canais específicos de comunicação, como e-mail. Essas interações me proporcionam uma visão clara do impacto que a evolução contínua dos produtos tem no sucesso da empresa e no valor entregue aos clientes. Este artigo explora como a colaboração entre times e o foco no cliente impulsionam a evolução dos nossos produtos, promovendo o crescimento do Grupo Softplan e o sucesso daqueles que utilizam nossas soluções. Colaboração entre times: o motor da inovação O desenvolvimento e a melhoria de produtos no Grupo Softplan exigem uma integração contínua entre diversos times. As soluções de software precisam ser eficazes e alinhadas às demandas do mercado. Embora não faça parte diretamente da definição do roadmap, minha função permite trazer insights valiosos com base em interações com clientes e dados de performance. Isso contribui diretamente para a priorização de iniciativas de desenvolvimento. Estudos da Forbes indicam que empresas que incentivam a colaboração interna têm 4,5 vezes mais chances de reter os melhores talentos e inovar com mais eficiência (Forbes on Collaboration). No Grupo Softplan, a colaboração eficaz é um dos pilares para garantir que as necessidades dos clientes sejam atendidas com agilidade e eficiência. As áreas de produto, marketing, growth e comercial trabalham juntas continuamente, sempre buscando alinhar as iniciativas com as demandas do mercado. Esse trabalho colaborativo, aliado ao apoio da equipe de Growth na priorização das iniciativas, integra diferentes perspectivas e áreas da empresa, permitindo ajustes constantes nos produtos e impulsionando a criação de inovações a partir dessas interações entre departamentos. Cliente como protagonista: o guia das nossas decisões No Grupo Softplan, o cliente está no centro de todas as decisões, especialmente na Unidade de Indústria e Construção, onde o valor de "cliente como protagonista" orienta nossa forma de trabalho. Utilizamos canais específicos para coletar feedbacks contínuos, e esses insights moldam as iniciativas de produto. Conforme apontado pela Salesforce, 80% dos clientes consideram a experiência oferecida por uma empresa tão importante quanto seus produtos e serviços (Salesforce State of the Connected Customer). Na prática, isso significa que, ao ouvir os usuários e ajustar nossos produtos com base em suas demandas, fortalecemos o relacionamento e aumentamos a lealdade à nossa marca. Um exemplo disso foi a recente atualização de funcionalidades, baseada no feedback dos clientes, que trouxe comunicações mais diversificadas em módulos do produto, alinhando-se às necessidades identificadas. Essa abordagem orientada pelo cliente não apenas atende às necessidades atuais, mas também nos permite antecipar demandas futuras. Esse processo contínuo solidifica nosso papel como parceiro estratégico para os clientes. Impacto no mercado: inovação e crescimento O Grupo Softplan se destaca no mercado por seu compromisso com a inovação e foco em resultados concretos. Ajustar nossos produtos com base no feedback direto dos clientes tem um impacto direto no crescimento da empresa e na satisfação dos usuários. Como mencionado, a atualização das comunicações mais diversificadas em módulos do produto foi uma resposta direta a esses feedbacks, destacando como a comunicação contínua com o cliente guia a evolução das nossas soluções. De acordo com a PwC, empresas que priorizam a experiência do cliente podem ver um aumento de 16% na receita e uma maior retenção de clientes (PwC Future of Customer Experience Survey). Essa realidade também se aplica ao Grupo Softplan, onde o ajuste contínuo e o foco nas necessidades dos clientes nos ajudam a entregar soluções relevantes que se destacam no mercado. O uso estratégico do feedback do cliente não só melhora a experiência do usuário, como também garante que estamos sempre um passo à frente em termos de inovação e competitividade. Venha crescer com a gente O Grupo Softplan se destaca por escutar seus clientes e unir seus times para criar soluções que impulsionam os negócios. O valor de "cliente como protagonista" é um guia prático e presente em nossa jornada de evolução de produtos. Colaboramos, inovamos e nos adaptamos, sempre garantindo que as necessidades dos clientes estejam no centro das nossas decisões. Se você valoriza um ambiente que fomenta colaboração e inovação, com oportunidades de aprendizado contínuo e crescimento, o Grupo Softplan é o lugar certo para você. Aqui, nossos valores e objetivos estratégicos são reforçados por treinamentos e pela oportunidade de trabalhar em projetos desafiadores que transformam o mercado de software. Junte-se a nós e faça parte de uma equipe que transforma a vida dos clientes e inova o mercado. Acesse nossa página de carreira.

A Evolução Digital no Setor Público: Gestão de Produtos B2G
Tech Writers Abril 16, 2025

A Evolução Digital no Setor Público: Gestão de Produtos B2G

Nos últimos anos, o setor público tem ampliado seus serviços digitais para os cidadãos. A pandemia de 2020 acelerou essa tendência, impulsionando a modernização de órgãos como Tribunais de Justiça, Ministérios Públicos e Defensorias. Essa transformação visa melhorar a eficiência dos serviços públicos e facilitar o acesso da população. Historicamente, as áreas de Tecnologia da Informação desses órgãos adotaram modelos de gestão de projetos que priorizam a entrega de escopos definidos, com prazos e equipes limitadas às demandas específicas. Contudo, a necessidade crescente de agilidade tem impulsionado a transição para a gestão de produtos. Nesse contexto, o conceito de Business-to-Government (B2G) ganha relevância, destacando a importância da gestão de produtos na oferta de soluções inovadoras para o governo. Como Product Manager atuando em produtos B2G, meu foco é entregar soluções alinhadas às necessidades dos usuários finais. Diferente do setor B2B, onde há um funil de vendas estruturado, a gestão de produtos no setor público exige a adoção de métricas e ferramentas adaptadas a esse ecossistema. Dia a dia de Products Managers no setor público As interações com os clientes iniciam-se após a assinatura do contrato, quando o primeiro contato ocorre com o grupo gestor, formado por servidores responsáveis pela implantação do produto. A partir desse ponto, são obtidos insights sobre as necessidades dos usuários finais, permitindo um entendimento inicial do fluxo de trabalho. Para priorizar o backlog, utilizamos a matriz RICE (Reach, Impact, Confidence, Effort), garantindo que as decisões considerem tanto as exigências contratuais quanto as necessidades dos usuários. Essa priorização ocorre de forma contínua, acompanhando a evolução do produto e dos contratos estabelecidos. No ciclo de desenvolvimento, aplicamos técnicas de experimentação, prototipação e testes de usabilidade com grupos pilotos. Coletamos dados quantitativos e qualitativos para medir a adesão e definir melhorias nas funcionalidades do produto. Exemplo de matriz RICE Com estas premissas em mente, aplicamos aos usuários piloto por exemplo, técnicas de experimentação, prototipação, condução de testes de usabilidade para novas funcionalidades. Também realizamos coletas constantes de dados quantitativos e qualitativos da jornada utilizada por eles, conforme aumenta a adesão. A partir das métricas coletadas, podemos definir se as funcionalidades principais ou acessórias da jornada do nosso usuário precisam ser evoluídas. Exemplo de insights quantitativos com informações da jornada do usuário na organização de tarefas utilizando a ferramenta MixPanel Exemplo de uso da ferramenta INDECX para informações qualitativas a respeito do produto ou funcionalidade Tríade de produto entregando resultados eficientes A gestão de produtos no setor público exige uma abordagem colaborativa, integrando time técnico, equipe de experiência do usuário e cliente. Essa interação contínua fortalece o alinhamento estratégico e a clareza sobre a evolução do produto. O roadmap do produto é compartilhado com o cliente para garantir transparência e previsibilidade nas entregas. Tríade de produto O Grupo Softplan se consolidou como referência na transformação digital do setor público, gerando impactos positivos para os cidadãos. Soluções como o Sistema de Automatização da Justiça (SAJ) proporcionam eficiência e celeridade nos serviços públicos. Como Product Manager na Softplan, contribuo para a gestão de produtos voltados ao setor público. Um exemplo é o SAJ Defensorias, cujo painel de tarefas foi desenvolvido após estudo de negócios e análise técnica baseada na tríade de produtos. Esse painel centraliza atividades diárias, priorizando tarefas a serem executadas imediatamente e organizando as finalizadas para referência futura. Solução SAJ Softplan Nosso objetivo é oferecer produtos intuitivos e eficientes, que atendam às demandas cotidianas dos defensores públicos e contribuam para a melhoria da prestação de serviços à sociedade. Painel de tarefas do Defensor no SAJ Defensorias As iniciativas digitais no setor público têm grande potencial de crescimento, impulsionadas pela cultura de produtos. A transformação digital é irreversível e continuará evoluindo para atender às expectativas da sociedade por serviços mais ágeis, eficientes e transparentes. Referências Bibliográficas CAGAN, M. Inspired: How to Create Tech Products Customers Love. United States: Wiley, 2017. EIS, D. Gestão Moderna de Produtos Digitais. Santa Catarina: Clube de Autores, 2022. TORRES, J. Gestão de produtos de software: Como aumentar as chances de sucesso do seu software. São Paulo: Casa do Código, 2015. Software Product Manager Responsibilities in Software Management Cycle https://asperbrothers.com/blog/software-product-manager-responsibilities/The different types of ‘Product Managers’: which one am I and which one do I need?https://medium.com/@carlosbeneyto/types-of-product-manager-startup-3bb978f50d2f Matriz Rice: o que é e como melhorar a priorização dos seus projetos: https://www.nomus.com.br/blog-industrial/matriz-rice/ Aprenda sobre NPS e satisfação dos clientes: https://blog.indecx.com.br/relatorios-de-nps-16-exemplos-com-indicadores-de-satisfacao-de-clientes/

Exaustão do ThreadPool do .Net
Tech Writers Março 25, 2025

Exaustão do ThreadPool do .Net

Mais de uma vez em minha carreira me deparei com este cenário: a aplicação .Net frequentemente mostrando tempos de resposta elevados. Esta alta latência pode ter várias causas, como lentidão no acesso a algum recurso externo (um banco de dados ou uma API, por exemplo), uso de CPU “batendo” em 100%, sobrecarga de acesso a disco, entre outras. Quero adicionar à lista anterior outra possibilidade, muitas vezes pouco considerada: exaustão do ThreadPool. Será apresentado de forma bem rápida como o ThreadPool do .Net funciona, e exemplos de códigos onde isto pode acontecer. Por fim, será demonstrado como evitar este problema. O ThreadPool do .Net O modelo de programação assíncrona baseado em Tasks (Task-based asynchronous programming) do .Net é bastante conhecido pela comunidade de desenvolvimento, mas acredito que seja pouco compreendido em seus detalhes de implementação - e é nos detalhes onde mora o perigo, como bem diz o ditado. Por trás do mecanismo de execução de Tasks do .Net existe um Scheduler, responsável, como seu nome já indica, por escalonar a execução das Tasks. Salvo alguma mudança explícita, o scheduler padrão do .Net é o ThreadPoolTaskScheduler, que também como o nome indica, utiliza o ThreadPool padrão do .Net para realizar seu trabalho. O ThreadPool gerencia então, como já se esperava, um pool de threads, para as quais ele atribui as Tasks que recebe usando uma fila. É nesta fila onde as Tasks ficam armazenadas até que haja um thread livre no pool, para então iniciar seu processamento. Por padrão, o número mínimo de threads do pool é igual ao número de processadores lógicos do host. E aqui está o detalhe em seu funcionamento: quando existem mais Tasks a serem executadas do que o número de threads do pool, o ThreadPool pode esperar uma thread ficar livre ou criar mais threads. Se escolher criar uma nova thread e se o número atual de threads do pool for igual ou maior que o número mínimo configurado, este crescimento demora entre 1 e 2 segundos para cada nova thread adicionada ao pool. Observação: a partir do .Net 6 foram introduzidas melhorias neste processo, permitindo que haja um aumento mais rápido no número de threads do ThreadPool, mas ainda assim a ideia principal se mantém. Vamos a um exemplo para deixar mais claro: suponha um computador com 4 cores. O valor mínimo do ThreadPool será 4. Se todas as Tasks que chegarem processarem rapidamente seu trabalho, o pool poderá inclusive ter menos do que o mínimo de 4 threads ativas. Agora, imagine que 4 Tasks de duração um pouco mais longa chegaram simultaneamente, utilizando então todas as threads do pool. Quando a próxima Task chegar à fila, ele precisará esperar entre 1 e 2 segundos, até que uma nova thread seja adicionada ao pool, para então sair da fila e começar a processar. Se esta nova Task também tiver uma duração mais longa, as próximas Tasks esperarão novamente na fila e precisarão “pagar o pedágio” de 1 a 2 segundos antes de poderem começar a executar. Se esse comportamento de novas Tasks de longa duração se mantiver por algum tempo, a sensação para os clientes deste processo será de lentidão, para qualquer nova tarefa que chegar à fila do ThreadPool. Este cenário é chamado de exaustão do ThreadPool (ThreadPool exhaustion ou ThreadPool starvation). Isso ocorrerá até que as Tasks finalizem seu trabalho e comecem a retornar as threads ao pool, possibilitando a redução da fila de Tasks pendentes, ou que o pool consiga crescer suficientemente para atender à demanda atual. Isso pode demorar vários segundos, dependendo da carga, e só então a lentidão observada anteriormente deixará de existir. Código síncrono x assíncrono É preciso agora fazer uma distinção importante sobre tipos de trabalho de longa duração. Geralmente eles podem ser classificados em 2 tipos: limitados pela CPU/GPU (CPU-bound ou GPU-bound), como a execução de cálculos complexos, ou limitados por operações de entrada/saída (I/O-bound), como o acesso a bancos de dados ou chamadas à rede. No caso de tarefas CPU-bound, salvo otimizações de algoritmos, não há muito o que fazer: é preciso ter processadores em quantidade suficiente para atender à demanda. Mas, no caso de tarefas I/O-bound, é possível liberar o processador para responder a outras requisições enquanto se espera a finalização da operação de I/O. E é exatamente isso que o ThreadPool faz quando APIs assíncronas de I/O são usadas. Neste caso, mesmo que a tarefa específica ainda seja demorada, a thread será retornada para o pool e poderá atender a uma outra Task da fila. Quando a operação de I/O finalizar, a Task será enfileirada novamente para então continuar a executar. Para saber mais detalhes sobre como o ThreadPool aguarda o fim de operações de I/O, clique aqui. Entretanto, é importante observar que ainda existem APIs síncronas de I/O, que causam o bloqueio da thread e impedem sua liberação para o pool. Estas APIs - e qualquer outro tipo de chamada que bloqueie uma thread antes de retornar a execução - comprometem o bom funcionamento do ThreadPool, podendo causar sua exaustão quando submetidos a cargas suficientemente grandes e/ou longas. Podemos dizer então que o ThreadPool – e por extensão o ASP.NET Core/Kestrel, desenhados para operar assincronamente – é otimizado para execução de tarefas de baixa complexidade computacional, com cargas I/O bound assíncronas. Neste cenário, um pequeno número de threads é capaz de processar um número bastante elevado de tasks/requisições de maneira eficiente. Bloqueio de threads com ASP.NET Core Vamos ver alguns exemplos de código que causam o bloqueio de threads do pool, usando ASP.NET Core 8. Observação: estes códigos são exemplos simples, que não visam representar nenhuma prática, recomendação ou estilo em especial, exceto os pontos relacionados à demonstração do ThreadPool especificamente. Para manter o comportamento idêntico entre os exemplos, será usado uma requisição a banco de dados SQL Server que simulará uma carga de trabalho que demora 1 segundo para retornar, usando a sentença WAITFOR DELAY. Para gerar uma carga de utilização e demonstrar os efeitos práticos de cada exemplo, utilizaremos o siege, um utilitário de linha de comando gratuito destinado a esta finalidade. Em todos os exemplos será simulada uma carga de 120 acessos concorrentes durante 1 minuto, com um atraso aleatório de até 200 milissegundos entre as requisições. Estes números são suficientes para demonstrar os efeitos sobre o ThreadPool sem gerar timeouts no acesso ao banco de dados. Versão Síncrona Vamos começar com uma implementação completamente síncrona: A action DbCall é síncrona, e o método ExecuteNonQuery do DbCommand/SqlCommand é síncrono, portanto, bloqueará a thread até que haja o retorno do banco de dados. Abaixo está o resultado da simulação da carga (com o comando siege utilizado). Vejam que conseguimos uma taxa de 27 requisições por segundo (Transaction rate), e um tempo de resposta médio (Response time) de cerca de 4 segundos, com a requisição mais longa (Longest transaction) durando mais de 16 segundos – um desempenho bastante precário. Versão Assíncrona – Tentativa 1 Vamos agora utilizar uma action assíncrona (retornando Task<string>), mas ainda utilizar o método síncrono ExecuteNonQuery. Executando o mesmo cenário de carga anterior, temos o seguinte resultado. Vejam que o resultado foi ainda pior neste caso, com taxa de requisições de 14 por segundo (contra 27 da versão completamente síncrona) e tempo médio de resposta de mais de 7 segundos (contra 4 da anterior). Versão Assíncrona – Tentativa 2 Nesta próxima versão, temos uma implementação que exemplifica uma tentativa comum – e não recomendada – de transformar uma chamada de I/O síncrona (no nosso caso, o ExecuteNonQuery ) em uma “API assíncrona”, usando Task.Run. O resultado, após a simulação, mostra que o resultado é próximo da versão síncrona: taxa de requisições de 24 por segundo, tempo médio de resposta de mais de 4 segundos e requisição mais longa levando mais de 14 segundos para retornar. Versão Assíncrona – Tentativa 3 Agora a variação conhecida como “sync over async”, onde utilizamos métodos assíncronos, como o ExecuteNonQueryAsync deste exemplo, mas é chamado o método .Wait() da Task retornada pelo método, como mostrado abaixo. Tanto o .Wait() quanto a propriedade .Result de uma Task tem o mesmo comportamento: causam o bloqueio da thread em execução! Executando nossa simulação, podemos ver abaixo como o resultado também é ruim, com taxa de 32 requisições por segundo, tempo médio de mais de 3 segundos, com requisições chegando a levar 25 segundos para retornar. Não à toa, o uso de .Wait() ou .Result em uma Task é desaconselhado em código assíncrono. Solução do problema Finalmente, vamos ao código criado para funcionar da forma mais eficiente, através do de APIs assíncronas e aplicando async / await corretamente, seguindo recomendação da Microsoft. Temos então a action assíncrona, com a chamada ExecuteNonQueryAsync com await. O resultado da simulação fala por si só: taxa de requisições de 88 por segundo, tempo médio de resposta de 1,23 segundos e requisição levando no máximo 3 segundos para retornar - números em geral 3 vezes melhores que qualquer opção anterior. A tabela abaixo sumariza os resultados das diferentes versões, para uma melhor comparação dos dados entre elas. Versão do códigoTaxa de requisições ( /s)Tempo médio (s)Tempo máximo (s)Síncrona27,384,1416,93Assíncrona 114,337,9414,03Assíncrona 224,904,5714,80Assíncrona 332,433,5225,03Solução88,911,233,18 Solução paliativa Vale mencionar que podemos configurar o ThreadPool para ter um número mínimo de threads maior do que o padrão (o número de processadores lógicos). Com isto, ele conseguirá rapidamente aumentar o número de threads sem pagar aquele “pedágio” de 1 ou 2 segundos. Existem pelo menos 3 formas para se fazer isto: por configuração dinâmica, usando o arquivo runtimeconfig.json, por configuração do projeto, ajustando a propriedade ThreadPoolMinThreads, ou por código, chamando o método ThreadPool.SetMinThreads. Isto deve ser encarado como uma medida temporária, enquanto não se faz os devidos ajustes em código como mostrado anteriormente, ou após os devidos testes prévios para confirmar que traz benefícios sem efeitos colaterais de desempenho, conforme recomendação pela Microsoft. Conclusão A exaustão do ThreadPool é um detalhe de implementação que pode trazer consequências inesperadas. E que podem ser difíceis de detectar se considerarmos que .Net possui várias maneiras de obter o mesmo resultado, mesmo em suas APIs mais conhecidas – acredito que motivado por anos de evoluções na linguagem e do ASP.NET, sempre visando compatibilidade retroativa. Quando falamos de funcionamento em taxas ou volumes crescentes, como ao passar de dezenas para centenas de requisições, é essencial conhecer as práticas e recomendações mais recentes. Além disso, conhecer um ou outro detalhe de implementação pode ser um diferencial para se evitar problemas de escala ou diagnosticá-los mais rapidamente. Fique de olho nas próximas publicações do Proud Tech Writers. Em um próximo artigo, vamos explorar como diagnosticar a exaustão do ThreadPool e identificar a origem do problema em código a partir de um processo em execução.