云原生应用要素之我见

        企业级业务系统开发技术从早期面向服务的架构、以RESTful API为中心的架构到如火如荼的微服务架构,再到如今的云原生应用架构,架构思想高于一切,特别在Java开发领域,前些年如果你不知道微服务概念,不知道SpringCloud、Dubbo和ServiceMesh领域的Istio、Linkerd,那你就太Low了,在我的工作经验里大概是2014年到2018年间。当我还没熟练掌握SpringCloud和Dubbo框架的时候,云原生应用概念又悄然出现在一线城市同事们的微信朋友圈,2020年之后我又跟风云原生应用技术,把它引入公司的业务平台中,回头看它到底起了多大作用,总体上也说不清楚。

        到底什么系统是符合云原生应用的系统,不同时期有不同的定义,如果你从最新最近的定义和原则去理解,有时有一点云里雾里,我喜欢从最早的定义和原则去理解,因为容易理解,因为有时适合中小企业,因为最新技术可能都是超大企业在玩,他们都还没完透,我们贸然看齐很容易完全不能消化,所以,这里我们从最早的云原生应用要素来理解。

        Heroku的创始人Adam Wiggins于2012年发布了“The Twelve-Factor APP”,即云原生应用十二要素。 之后,同样作为云计算领导者,Pivotal重新整理了Beyond the 12 factor App, 即云原生15要素。

要素1:  One Codebase, One Application

       Heroku版本:一份基准代码,多份部署。

        一个代码库,一个应用 。

       理解:

        1)一个应用会出现几份代码么?一个应用在多个客户环境部署,会引诱开发人员形成多份代码,在git仓库里创建了多个分支,然后各自向前迭代,最后永远都不能merge了。

        要素1要求我们一个应用,保持一份代码,也就是一个Git仓库,一个master主干,所有分支都是要merge到主干的,然后发布到tag的;这一份代码,可以支持多环境部署的,在多个相同客户环境都是同一个安装包,不同的是环境变量和客户配置。

        2)一份代码会包含几个应用么?那就要看你怎么定义什么是一个应用,如果是一个平台是一个应用,我想一般不会;当前一般平台都是分布式部署的,很少一个企业平台不是分布式系统的,那么分布式系统每个子系统或者每个组件,都认为是一个应用,这些子系统和组件,要做到一份代码只包含一个子系统或一个组件,如果包含了两个以上的,则表示包含几个应用。

        要素1要求我们一份代码,只包含一个应用,应用就是分布式系统下的子系统或组件?看似简单,其实我认为最考究架构师的水平,子系统或组件要怎么划分,这个粒度问题是需要架构师去根据实际情况把握的,其实也就是微服务的划分问题。别人说用户管理、角色权限管理都要各自做成一个微服务组件,难道每个微服务系统都这么干么?

        当面对一个ToC的互联网系统,我们有1亿+的用户账号,而且日访问量很大,那么用户管理做成一个独立的组件是合适的。

        当面对一个ToB的传统政企系统,用户就是几十个根本很少访问系统的政企人员,那么用户管理就不合适做成一个独立的组件,我们可能把一个用户中心做成一个子系统或组件,把用户管理、权限管理、角色管理作为功能模块纳入这个组件里。

要素2 API First

        API优先, 或者说API在设计上是第一级考虑的。

        什么是API?Application Programming Interface, 即应用程序接口。

        曾经有以RESTful API为中心的架构设计,其实云原生技术是一个集大成的技术,它包含了微服务技术、RESTful API技术等。

        我们做微服务系统时,微服务系统就只负责后台系统,前端是独立的,用现代技术框架React、Vue等来开发的,做到了前后端分离,它们之间的交互模式就是通过API来实现的,由于前端都是浏览器运行的,天然支持HTTP协议的,所以通常都是基于HTTP或HTTPS的RESTful API来实现的,数据封装基本是JSON。

        微服务系统里有很多组件,微服务网关和这些实现了具体业务逻辑的业务组件之间的通讯,以及这些业务组件之间的通讯,也是要通过API来交互的,通常走TCP的RPC调用,当然也可以走HTTP协议调用,看你对性能的要求。

        RPC开源框架流行的有:Dubbo,  Thrift、gRPC等,这些框架都具有自己特有的数据封装协议,比如著名的google protobuf和thrift,即使你不用它们的整套框架,也可以使用这些数据包编码解码库功能,我们在某个系统里就用google protobuf来做数据包封装,然后自己加密传输。

        最低层次就是代码级的API调用,本来在C/C++编程中把API接口就定义在.h头文件里,然后把实现写在.c或.cpp文件里,整个编译打包成.dll或.so文件,给上层系统调用。Java编程中也是类似做法,把一个小组件打包成jar供上层系统调用,maven对这些管理的很好,大家引用第三方jar都习惯到麻木。

        这里明显有两种模式,一个是进程间的API交互,一个是进程内的API交互,我们更关注进程间的API交互方式,在稳定性上、性能上、灵活性上都需要考虑清楚。

        即使我们不搞云原生、不搞微服务,API服务技术思想也是很重要的,在分布式系统、在模块化设计、在前后端分离架构上,API都是很值得我们去好好思考和设计应用的,设计的好会解耦得很好,反之则系统一团乱麻。

        

要素3 Dependency Management

        Heroku版本:显式声明依赖关系。

        既然讲到了显式依赖,肯定就有隐式依赖。

        显式依赖就是在编译清单中明确记录了依赖的库或包名称、版本等,反之就是隐式依赖。比如Java开发中的依赖管理工具Maven支持的pom.xml文件,Go开发中的go.mod文件,npm工具支持的package.json文件,这三个文件都把工程需要的库或包名称、版本号写清楚了,这种做法就是显示依赖。

        显式依赖对于我们都司空见惯,以至于隐式依赖都想不出例子来,经典的例子要回到C/C++开发中来,一个程序调用dll库或so库或静态库,常常给定的库名,并没有指定版本号,特别是哪些操作系统上的库或安装的第三方库,如果你在一个操作系统版本上开发调测,然后到另一个版本的操作系统上运行,刚好你引用的库在两个操作系统上版本完全不一样,刚好一个实现不是你预期的,那会引发怎样的运行时后果就不得而知了。

        在这一点上,云原生直截了当用docker技术对运行时环境进行终极解决,把应用程序直接打包成docker镜像进行发布,所需要的依赖全部包含了,跟目标环境操作系统上的各种库没有直接依赖关系了,当然就不存在任何问题了,比如在一台操作系统上,你的不同程序可以运行在不同版本的JDK上,因为JDK都被docker镜像自带了。

        docker技术真的是伟大的进步,没有它,我们用虚拟机vmware来发布一个应用程序,一个应用程序一个vmware虚拟机,在资源消耗方面几乎不可想象。当然你可以说我不用docker,我用的是某某技术,其实你不过是用了另一个docker来解决问题而已。

要素4 Design, Build, Release, Run

        Heroku版本:严格分离构建和运行。

        现在讲究的是敏捷开发、持续集成和持续交付, 所谓的CI/CD,早期出名的是Jenkins,现在是GitLab上CICD,从代码库的各个分支和标签上就进行CICD,实在是非常方便。

        早期我们用SVN来管理代码仓库,之后引入GitLab系统来管理代码仓库,在CI/CD pipelines上跑Build, Release, Run,管道步骤是可以自己灵活定义的,每一步都可以去gitlab-runner上去执行自己定义的流程,比如执行一个shell脚本。

        在gitlab-runner里,我们在docker容器里编译C, C++, Go,Java等程序,或者直接在gitlab-runner里跑shell脚本,从拉取代码、编译、生成镜像、打包; 如果是测试需要,则解包,部署到docker集群、Hadoop YARN集群或scp+ssh进行跨机器远程部署,测试人员就是一个按钮下去,一切都是自动处理。

        发布和线上部署,这里分两种情况:

        一种就是你的环境是在互联网上,或者说你坐在办公室里能够访问到的任何网络,恭喜你,你可以和公司内部测试环境一样,一键自动搞定。

        另一种就是你根本无法进入到线上环境,因为你需要穿过很多个网络之后才能达到线上环境,而且都是只准进不准出的单向网络,比如单向FTP传输,更让你头痛的是有一两个关卡,必须要人工用专门U盘拷贝来跨网,为的是尽可能的安全,按要素1要求,还必须一个安装包分发到多个地市客户环境,那么这种自动+人工的发布和部署管道,必须自己按实际情况实现,开源软件基本没有现成的来解救你。

要素5 Configuration, Credentials and Code

       Heroku版本:在环境中配置存储。

        在环境中存储配置,不同的环境具有不同的配置,这和要素1是配套的,代码只有一份,但配置是多份,一个客户环境一份专门的配置。

        在环境中保存各种登录认证凭证,不要把这些凭证信息保存在代码中,也就是除了这个环境的运维人员自己知道这些客户环境的凭证以外,公司的市场、产品、开发和测试人员都不知道,这也是安全的最小化原则,不该知道的人尽量不要让他知道。

        标准做法是使用配置中心来管理,如Consul、Etcd, Nacos等。

        应用程序读取环境变量,由docker compose文件注入,每个部署环境都有自己的环境变量定义文件。

        应用程序读取配置中心的配置文件,由remco工具来实时拉取更新的配置文件,重启应用程序,应用程序本身并不知道配置在某个配置中心系统里,它永远只处理本地目录下的配置文件。通过remco工具来解耦,让各个部分更方便灵活,比如开发做单元测试,永远都是读取本地yml配置文件,不需要去访问配置中心。

        这里有一个问题:让运维人员去管理这些配置和凭证,常常由于运维人员的问题而导致问题,具体操作上也是一个挑战。

        凭证涉及安全,安全本身就是一个极大的挑战,系统做安全了同时就是给运维带来了麻烦,实际未必认真去做,安全措施和流程在那里却不认真做,最后比没有安全措施还差的效果。比如安全要求数据库登录账号要设置过期时间,但是到期时没有很好的通知机制,最后运维也忘记了,直到客户发现系统无法访问数据,造成了损失才来解决。

要素6 Logs

        Heroku版本:把日志当做事件流。

        系统运行日志一直都有两种方式:屏幕打印和写入文件。屏幕打印就是标准输出stdout和标准出错stderr, 写文件方式有著名的Log4J,  各个语言都有成熟的组件库。

        以前采用物理机上运行程序时,都是强调要用专业的日志文件方式,如Log4J,要设置文件大小和文件数量,反而到了docker容器中运行,却建议直接采用屏幕打印,这其实是利用了docker的日志机制。

        如果我们写到当前目录下,日志文件就在容器里,外部是很难获取的;如果我们直接标准输出,docker容器会把日志统一写到/var/lib/docker/containers目录下,所以我们不用伤脑筋去考虑日志文件名是什么以及外部怎么能方便查看的问题。其实本质上还是写文件,但细节不一样。

        为什么说要把日志当做事件流呢?因为对一个分布式系统,一单业务处理可能穿过好几个微服务组件,这些容器有自己的日志文件,那么我们要分析处理情况,只有把这些日志按时间顺序汇总到日志中心去,然后在日志中心按时间顺序进行分析查看,就能发现业务逻辑问题,这种处理方式是不是一种按时间来处理的事件流呢。

        这个采集日志、传输日志、聚合入库日志、查看日志的软件体系有现成的开源系统,著名的有ELK+filebeat,即filebeat、logstash、Elasticsearch和Kabana。早期我们也是采用ELK体系,后来放弃了,原因不是技术问题,就是ELK开销比较大,Java的系统都吃内存,谁说CPU、内存和磁盘都是白菜价的,那是他没有碰到成千上万个传感器采集设备日日夜夜上传图片、声音、视频,项目经费就那么些,客户要求至少半年存储量,留给你搞运维监控系统的资源基本就没有。

        因为要系统的指标监控,云原生开源系统首推Prometheus+Grafana,  既然这样干脆日志采集用Grafana Loki, 清一色采用Go语言开发的开源组件,在Grafana界面上统一看系统指标和运行日志,减少了一个web系统Kabana,其实我挺喜欢Kabana界面的。

        但是讲到最后,让人感慨的是,人的因素永远是最重要的,你再好的监控系统没有人认真去学习去使用去分析,一切都等于零,上面有讲到如果系统是运行在非互联网的封闭环境里,只有驻点的运维人员能天天接触,情况就更严重了,不能简单的说是一个环节的问题。

要素7  Disposability       

        Heroku版本:快速启动和优雅终止, 可最大化健壮性。

        云原生技术采用docker镜像技术把一个微服务组件程序打包成一个独立的单元,采用docker容器来独立运行,要快速启动,只要这个组件足够小,启动过程就会很快,现实中没有什么困难的地方,很容易做到。

        但是优雅终止真正做到不是那么容易,如果你用kill -9肯定就太暴力了,那么你的程序是否处理了正常程序退出信号呢,只有你自己知道。如果服务是RESTful API服务,被动执行一单单请求的,执行时间都很短,影响还小;如果服务里是一个后台执行批处理任务,去终止执行就会是一个问题,理论上大家会如何如何处理,实际上可能就是不管了,有太多的事情要做,这个问题大家都知道存在,谁也没时间去处理,相信不少企业存在这样的情况。

        优雅终止终将有一天会被处理得很好,但不是现在,所以这一天常常永远不会到来。

要素8 Backing Services

        Heroku版本:把后端服务当做附加资源。

        我的理解就是把系统用到的中间件和基础系统都当成服务,都通过某个标准协议来获取这些服务提供的资源和能力,这个也很自然而然,大家理解也不会存在问题,比如:

        数据库服务:可能是MySQL、PostGres等关系型数据库,我们通过SQL来访问。

        缓存服务:可能是Redis、memcached等。

        消息队列服务:可能是Kafka、RabbitMQ、RocketMQ等。

        对象存储服务:可能是公有云提供的对象存储,可能是自建的MinIO、SeaweedFS、Hadoop Ozone等。

        特征向量服务:可能是milvus、vearch、Jina等。

要素9 Environment Parity

        Heroku版本:尽可能的保持开发、预发布、线上环境相同。

        对开发、测试和各个客户环境,都建立一样的docker集群环境和后端服务,从gitlab CI/CD打包出来的镜像文件,分发到各个环境去运行,保证二进制包只有一个,保持开发、测试、生产环境的部署架构、程序版本等都是一致的。

        

要素10 Adminstrative Process

        Heroku版本:后台管理任务当作一次性进程运行。

        有别于常规业务任务,后台管理任务都是一次性运行就结束,也就是不要做一个后台长时间运行的任务进程。

        一般都有一个任务调度系统来调度这些任务,比如Azkaban、Oozie等,很多公司可能会开发一个自己的任务调度系统。

要素11 Port Binding

        Heroku版本:通过端口绑定提供服务

        一般在docker容器里运行的服务可以是一个固定端口向外提供服务,如80端口,然后通过端口映射配置一个外部可用的端口,达到了灵活性和解耦。

        如果最终向外部提供的端口,只想统一用一个固定端口,如80端口,是否可以做到?用微服务网关可以解决这一问题,各个内部服务用路径来区分。如采用Traefik来做网关,在docker-compose.yml文件里做部署标签声明,把一个路径前缀映射到这个容器提供的服务端口。

要素12 Stateless Processes

        Heroku版本:以一个或多个无状态进程运行应用。

        应用是有状态的,但是服务要是无状态的。应用的状态一般存储在后端服务里,如缓存服务中、数据库服务中,应用前端保存一个有过期时限的令牌。

        那么这些无状态的服务是很容易横向扩展和收缩的。

要素13 Concurrency

        Heroku版本:通过进程模型进行扩展。

        服务可以通过进程进行扩展,即通过多部署服务器节点,以及在一个节点上多部署进程实例,就可以提升系统的并发处理能力。

        当然同一个进程也不是不可以有多线程,而是强调系统架构能支持多进程模式,docker容器集群机制和要素12就可以轻松保证这一点。

要素14 Telemetry

        所谓遥测,就是对服务的监控和健康检查有一套机制,运维人员不需要一个个进入容器内检查,这套机制通过部署各种各样的采集探针来实时收集系统健康指标,实时上传到监控中心,运维人员在监控中心就可以跟踪分析,也可以接收告警信息。

        云原生开源系统最著名的是Prometheus+Grafana体系,都是用Go语言开发的。

要素15 Authentication and Authorization

        整个平台的安全性问题要做好,这个也是一个难以做好的点,安全性和方便性是矛盾的,要做好安全性,开发人员就要付出很多精力。

        常常有几个方面要做:

        单点登录SSO,  所有系统只需要一次登录认证即可,比较安全的做法是采用Ukey+CA证书进行身份合法性和有效性认证。

        模块权限管理,对各个系统各个组件接口,做权限控制拦截。

        数据权限管理,对同一个接口查询,面对的数据进行权限管理,比如按部门进行权限控制,哪些部门能查询哪些范围的数据。

        这两方面的用户鉴权做起来其实很麻烦,常常开发人员就忘记了,形成一个业务上的权限漏洞。

结尾

        云原生定义就有几个版本,云原生技术涵盖了从开发、测试到运维的方方面面,感觉是智者见智仁者见仁,在实际实施上没有一个统一的标准和做法,我的经验和理论认识都很浅显,两方面都需要进一步提升。

        

        

        

你可能感兴趣的:(云原生,云原生,微服务,架构)