使用gRPC扩展Kubernetes并通过三行配置解决扩展问题 (使用graphpad做随时间变化图)
Introduction
The journey began with a simple question posed to our Senior Software Engineer: "Forget speed concerns. Doyou genuinely believe developing communication in gRPC is better than REST?" The immediate response I didn't want to hear came: "Absolutely yes."
Background
Prior to raising the question, I had been monitoring peculiar behavior in our services during rolling updates and Pod expansions. The majority of our microservices previously communicated via REST calls without any issues. We had migrated some of these integrations to gRPC, primarily to eliminate the overhead of REST. Recently, we observed a series of problems that all pointed in one direction - our gRPC communication.
Kubernetes Load Balancing
Of course, we followed the recommended practices for running gRPC in Kubernetes without a service mesh. On the server side, we used a headless Service object, and on the client side, gRPC employed client-side round-robin load balancing and discovery.
However, things took an interesting turn when we realized that the load balancer built into Kubernetes, at Layer 4, is not designed for load balancing RPCs. Layer 4 load balancers are common due to their simplicity, as they are protocol-agnostic. However, gRPC disrupts the connection-level load balancing provided by Kubernetes.
The reason for this is that gRPC is built on HTTP/2, which is designed to maintain a long-lived TCP connection where all requests can be active simultaneously at any point in time. This reduces the overhead of connection management. However, in this scenario, connection-level load balancing is not very useful since once a connection is established, there is no more load balancing required. All requests are pinned to the original destination Pod until a new DNS discovery (using headless Service) takes place. This doesn't happen until at least one of the existing connections is closed.
Example: Illustrating gRPC Load Balancing
Network I/O activity on a newly created Pod after the gRPC configuration change:
Configuration Tweaks
Two configurations resolved the issue, technically speaking, in one line. As mentioned earlier, we were using client-side load balancing with DNS discovery via a headless Service object. Other options could involve using proxy load balancing or implementing another discovery method that queries the Kubernetes API instead of DNS.
Additionally, the gRPC documentation provides a proposal for server-side connection management, which we also experimented with. Here's a sample Go code snippet for setting the following server parameters that I recommend you try:
grpc.KeepaliveParams(keepalive.ServerParameters{ MaxConnectionAge: time.Second 30, // THIS one does the trick MaxConnectionAgeGrace: time.Second 10, })
Real-World Behavior
The new Pod count after the gRPC configuration change:
Rolling Update Issues
With the expansion issues resolved, another problem became more evident. The focus shifted to gRPC
code=UNAVAILABLE
errors occurring on the client side during rolling updates. Strangely, this was only observed during the rolling update and not during individual Pod expansions.
The number of gRPC errors during the rolling update:
DNS Rediscovery
The rolling update process is straightforward: create a new replica set, create a new Pod, once the Pod is ready, terminate the old Pod from the old replica set, and so on. The start time between each Pod is spaced by 15 seconds.
We know that gRPC DNS rediscovery only initiates when the old connection is dropped or ends with a GOAWAY signal. So every 15 seconds, the clients initiate a new rediscovery, but they are met with stale DNS records. They then keep retrying the rediscovery until they succeed.
DNS TTL Cache
Except it wasn't a DNS issue... DNS TTL caching is prevalent in almost every place. The infrastructure DNS has its own cache. The clients suffer from their default 30-second TTL cache, while Go clients typically do not implement DNS caching. In contrast, Java clients reported hundreds or even thousands of occurrences of this issue.
Of course, we could reduce the time for the TTL cache, but why would this only affect gRPC during rolling updates?
Solution
Fortunately, there is an easy-to-implement workaround. Or rather, the solution is: have the new Pods start with a 30-second delay.
.spec.minReadySeconds = 30
The Kubernetes Deployment spec allows us to set a minimum amount of time a new Pod must be ready before it starts terminating old Pods. After this time, the connections areterminated.
Conclusion
The investigations and experiments conducted in this article have shed light on some of the challenges and potential issues that can arise when utilizing gRPC in a Kubernetes environment. While gRPC provides certain advantages over REST, such as improved performance and efficiency, it is important to consider the specific requirements and constraints of your application when making the choice between the two.
By understanding the intricacies of gRPC communication, connection management, and Kubernetes load balancing, you can effectively deploy and scale your gRPC-based services within Kubernetes.
Go - Micro微服务框架实践 - API(十三)
Micro的api就是api网关
API参考了 API网关模式 为服务提供了一个单一的公共入口。基于服务发现,使得micro api可以提供具备http及动态路由的服务。
Micro的API基于HTTP协议。请求的API接口通过HTTP协议访问,并且路由是基于服务发现机制向下转发的。 Micro API在go-micro 之上开发,所以它集成了服务发现、负载均衡、编码及基于RPC的通信。
因为micro api内部使用了go-micro,所以它自身也是可插拔的。 参考 go-plugins 了解对gRPC、kubernetes、etcd、nats、及rabbitmq等支持。另外,api也使用了 go-api ,这样,接口handler也是可以配置的。
ACME( Automatic Certificate Management Environment)是由 Let’s Encrypt 制定的安全协议。
可以选择是否配置白名单
API服务支持TLS证书
API使用带分隔符的命名空间来在逻辑上区分后台服务及公开的服务。命名空间及http请求路径会用于解析服务名与方法,比如 GET /foo HTTP/1.1 会被路由到 服务上。
API默认的命名空间是 ,当然,也可以修改:
我们展示一个3层的服务架构:
完整示例可以参考: examples/greeter
先决条件:我们使用Consul作为默认的服务发现,所以请先确定它已经安装好了,并且已经运行,比如执行 consul agent -dev 这样子方式运行。
向micro api发起http请求
HTTP请求的路径 /greeter/say/hello 会被路由到服务 的方法 上。
绕开api服务并且直接通过rpc调用:
使用JSON的方式执行同一请求:
micro api提供下面类型的http api接口
请看下面的例子
Handler负责持有并管理HTTP请求路由。
默认的handler使用从注册中心获取的端口元数据来决定指向服务的路由,如果路由不匹配,就会回退到使用”rpc” hander。在注册时,可以通过 go-api 来配置路由。
API有如下方法可以配置请求handler:
通过/rpc入口可以绕开handler处理器。
API处理器接收任何的HTTP请求,并且向前转发指定格式的RPC请求。
RPC处理器接收json或protobuf格式的HTTP POST请求,然后向前转成RPC请求。
代理Handler其实是内置在服务发现中的反向代理服务。
事件处理器使用go-micro的broker代理接收http请求并把请求作为消息传到消息总线上。
Web处理器是,它是内置在服务发现中的HTTP反向代理服务,支持web socket。
/rpc 端点允许绕过主handler,然后与任何服务直接会话。
示例:
更多信息查看可运行的示例: /micro/examples/api
解析器,Micro使用命名空间与HTTP请求路径来动态路由到具体的服务。
API命名的空间是 。可以通过指令 --namespace 或者环境变量 MICRO_NAMESPACE= 设置命名空间。
下面说一下解析器是如何使用的:
RPC解析器示例中的RPC服务有名称与方法,分别是 , 。
URL会被解析成以下几部分:
带版本号的API URL也可以很容易定位到具体的服务:
代理解析器只处理服务名,所以处理方案和RPC解析器有点不太一样。
URL会被解析成以下几部分:
Kubernetes是什么?
Kubernetes是什么? 首先,它是一个全新的基于容器技术的分布式架构领先方案。 这个方案虽然还很新,但它是谷歌十几年以来大规模应用容器技术的经验积累和升华的重要成果。 确切地说,Kubernetes是谷歌严格保密十几年的秘密武器——Borg的一个开源版本。 Borg是谷歌的一个久负盛名的内部使用的大规模集群管理系统,它基于容器技术,目的是实现资源管理的自动化,以及跨多个数据中心的资源利用率的最大化。 十几年以来,谷歌一直通过Borg系统管理着数量庞大的应用程序集群。 由于谷歌员工都签署了保密协议,即便离职也不能泄露Borg的内部设计,所以外界一直无法了解关于它的更多信息。 直到2015年4月,传闻许久的Borg论文伴随Kubernetes的高调宣传被谷歌首次公开,大家才得以了解它的更多内幕。 正是由于站在Borg这个前辈的肩膀上,汲取了Borg过去十年间的经验与教训,所以Kubernetes一经开源就一鸣惊人,并迅速称霸容器领域。 其次,如果我们的系统设计遵循了Kubernetes的设计思想,那么传统系统架构中那些和业务没有多大关系的底层代码或功能模块,都可以立刻从我们的视线中消失,我们不必再费心于负载均衡器的选型和部署实施问题,不必再考虑引入或自己开发一个复杂的服务治理框架,不必再头疼于服务监控和故障处理模块的开发。 总之,使用Kubernetes提供的解决方案,我们不仅节省了不少于30%的开发成本,还可以将精力更加集中于业务本身,而且由于Kubernetes提供了强大的自动化机制,所以系统后期的运维难度和运维成本大幅度降低。 然后,Kubernetes是一个开放的开发平台。 与J2EE不同,它不局限于任何一种语言,没有限定任何编程接口,所以不论是用Java、Go、C++还是用Python编写的服务,都可以被映射为Kubernetes的Service(服务),并通过标准的TCP通信协议进行交互。 此外,Kubernetes平台对现有的编程语言、编程框架、中间件没有任何侵入性,因此现有的系统也很容易改造升级并迁移到Kubernetes平台上。 最后,Kubernetes是一个完备的分布式系统支撑平台。 Kubernetes具有完备的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建的智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制,以及多粒度的资源配额管理能力。 同时,Kubernetes提供了完善的管理工具,这些工具涵盖了包括开发、部署测试、运维监控在内的各个环节。 因此,Kubernetes是一个全新的基于容器技术的分布式架构解决方案,并且是一个一站式的完备的分布式系统开发和支撑平台。 在正式开始本章的Hello World之旅之前,我们首先要学习Kubernetes的一些基本知识,这样才能理解Kubernetes提供的解决方案。 在Kubernetes中,Service是分布式集群架构的核心,一个Service对象拥有如下关键特征。 拥有唯一指定的名称(比如mysql-server)。 拥有一个虚拟IP(Cluster IP、Service IP或VIP)和端口号。 能够提供某种远程服务能力。 被映射到提供这种服务能力的一组容器应用上。 Service的服务进程目前都基于Socket通信方式对外提供服务,比如Redis、Memcache、MySQL、 Web Server,或者是实现了某个具体业务的特定TCP Server进程。 虽然一个Service通常由多个相关的服务进程提供服务,每个服务进程都有一个独立的Endpoint(IP+Port)访问点,但Kubernetes能够让我们通过Service(虚拟Cluster IP +Service Port)连接到指定的Service。 有了Kubernetes内建的透明负载均衡和故障恢复机制,不管后端有多少服务进程,也不管某个服务进程是否由于发生故障而被重新部署到其他机器,都不会影响对服务的正常调用。 更重要的是,这个Service本身一旦创建就不再变化,这意味着我们再也不用为Kubernetes集群中服务的IP地址变来变去的问题而头疼了。 容器提供了强大的隔离功能,所以有必要把为Service提供服务的这组进程放入容器中进行隔离。 为此,Kubernetes设计了Pod对象,将每个服务进程都包装到相应的Pod中,使其成为在Pod中运行的一个容器(Container)。 为了建立Service和Pod间的关联关系,Kubernetes首先给每个Pod都贴上一个标签(Label),给运行MySQL的Pod贴上name=mysql标签,给运行PHP的Pod贴上name=php标签,然后给相应的Service定义标签选择器(Label Selector),比如MySQL Service的标签选择器的选择条件为name=mysql,意为该Service要作用于所有包含name=mysql Label的Pod。 这样一来,就巧妙解决了Service与Pod的关联问题。 这里先简单介绍Pod的概念。 首先,Pod运行在一个被称为节点(Node)的环境中,这个节点既可以是物理机,也可以是私有云或者公有云中的一个虚拟机,通常在一个节点上运行几百个Pod;其次,在每个Pod中都运行着一个特殊的被称为Pause的容器,其他容器则为业务容器,这些业务容器共享Pause容器的网络栈和Volume挂载卷,因此它们之间的通信和数据交换更为高效,在设计时我们可以充分利用这一特性将一组密切相关的服务进程放入同一个Pod中;最后,需要注意的是,并不是每个Pod和它里面运行的容器都能被映射到一个Service上,只有提供服务(无论是对内还是对外)的那组Pod才会被映射为一个服务。 在集群管理方面,Kubernetes将集群中的机器划分为一个Master和一些Node。 在Master上运行着集群管理相关的一组进程kube-apiserver 、 kube-controller-manager和kube-scheduler,这些进程实现了整个集群的资源管理、Pod调度、弹性伸缩、安全控制、系统监控和纠错等管理功能,并且都是自动完成的。 Node作为集群中的工作节点,运行真正的应用程序,在Node上Kubernetes管理的最小运行单元是Pod。 在Node上运行着Kubernetes的kubelet、kube-proxy服务进程,这些服务进程负责Pod的创建、启动、监控、重启、销毁,以及实现软件模式的负载均衡器。 最后,看看传统的IT系统中服务扩容和服务升级这两个难题,以及Kubernetes所提供的全新解决思路。 服务的扩容涉及资源分配(选择哪个节点进行扩容)、实例部署和启动等环节,在一个复杂的业务系统中,这两个难题基本上靠人工一步步操作才得以解决,费时费力又难以保证实施质量。 在Kubernetes集群中,只需为需要扩容的Service关联的Pod创建一个RC(Replication Controller),服务扩容以至服务升级等令人头疼的问题都迎刃而解。 在一个RC定义文件中包括以下3个关键信息。 目标Pod的定义。 目标Pod需要运行的副本数量(Replicas)。 要监控的目标Pod的标签。 在创建好RC(系统将自动创建好Pod)后,Kubernetes会通过在RC中定义的LabelPod实例并实时监控其状态和数量,如果实例数量少于定义的副本数量,则会根据在RC中定义的Pod模板创建一个新的Pod,然后将此Pod调度到合适的Node上启动运行,直到Pod实例的数量达到预定目标。 这个过程完全是自动化的,无须人工干预。 有了RC,服务扩容就变成一个纯粹的简单数字 游戏 了,只需修改RC中的副本数量即可。 后续的服务升级也将通过修改RC来自动完成。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。