分类: 程序设计

通过WPeMatico自动添加.

  • 网络数字身份认证术

    网络数字身份认证术

    这篇文章是《HTTP API 认证授权术》的姊妹篇,在那篇文章中,主要介绍了 HTTP API 认证和授权技术中用到的 HTTP Basic, Digest Access, HMAC, OAuth, JWT 等各种方式,主要是 API 上用到的一些技术,这篇文章主要想说的是另一个话题——身份认证。也就是说,怎么确认这个数据就是这个人发出来的?

    用户密码

    要解决这个问题,我们先来看一个最简单的解——使用密码,通常来说,在网络上要证明一个人的身份的话,都需要这个人的一些私密而唯一的东西。比如,像密码这样的东西,很多地方,只要你提供了你的用户名+密码,就可以确定这个人是你(注明:关于密码管理,强密码设定,密码泄漏,密码破解以及密码哄骗不在这篇文章的话题中),也就是说,这个密码是非常私密的事,我们可以假设,这个事全世界只能有当事人一个人知道,所以,当事人得供正确的密码,我们就可以认证这个人了。

    为了加强密码的安全程度,一般会使用 2FA(Two-factor authentication)或 MFA(Multi-factor authentication),双因认证或多因认证,这需要用户提供一个唯一的可信设备,比如用户的手机,然后通过验证手机短信,或是像 Google Authenticator  这样的动态口令来完成。这样的安全级别已经算是比较高了。如果能够再加上经常性的变更密码,那么安全级别就更好了。

    另外,一些公司还使用了生物密码来进行用户的身份验证,比如人脸识别。但是,我个人觉得人脸识别或是生物识别是比较糟糕的方式,因为:

    • 目前能被验证的生物信息(如人脸和指纹)太容易被别人获得和伪造了。
    • 这样东西不能被变更和吊销,密码可以被吊销和重置,人脸则不能。

    密钥对和证书

    密码可以解决身证认证的问题有很多问题,最重要的一个问题就是,你要把你的密码提供给对方,对方才能验证你的身份。你不可能把你的密码提供给全世界的人吧,这样的话,全世界的人都有你的密码了,那么任何人都能变成你了。所以,用户密码这个事只能存在于权威机构和普通用户之间,不能存在于普遍应用中。所以,这里需要使用更好的解决方案。

    使用 ECC(Elliptic-Curve Cryptography)椭圆曲线密码术,可以通过一个“密钥对”进行非对称加密。这种技术,在对信息进行加密和解密时,使用两个不同的密钥,其中一个用来做加密,另一个做解密。这样一来,我们就可以把其中一个密钥公布出去,称之为公钥,另一个密钥私密地保管好,称之为私钥。

    比如,我用我的私钥加密信息,然后,我把这个私钥所配对的公钥发布给所有人,大家都用公钥解密信息,不用我的公钥你解密不了这个信息。这样一来,就可以保证这个信息是我发出来的,不但保证了信息安全,还完成了身份认证。

    keypair

    这样的现实案例一般用于网站,也就是用户得要知道我访问的这个网站是真实的,不是别人做的。因为 DNS 很容易被 hack,你连上一个不可信的网络,这个网络里的 DNS 把这个网站的 IP 地址解析成什么 就是什么了。但是有了这个加密的机制后,网站把自己的信息加密后连同公钥给到访问者,访问解密后就知道是不是这个网站了。

    但是,这里还是会有一个很严重的问题,那就是中间人攻击。如下图所示:

    中间人 Chad 把自己伪装成 Bob 向 Alice 要信息,然后,再伪装成 Alice 对 Bob 说,这就是 Alice 的公钥,于是 Bob 也无法验证是不是 Alice 的公钥,因为公钥里就是一堆乱七八糟的数据,我们完全不能分辨哪个公钥属于 Alice 的。试想,如果我们收到声称属于银行的密钥。我们怎么知道它确实属于你的银行?

    这里的答案就是使用数字证书。证书跟我们的身份证非常类似,其需要一个可信机构来颁发和验证的。这个证书机构 CA(Certificate Authority)是一个是大家都相信的权威机构,他用他的人品保证(当然一般会被严格管理和审计),CA 机构同样使用这样的非对称加密的技术来完成颁发和验证的事。下图展示了这一过程。

    certificate

    说明一下上面这个图:

    1. 为了解决公钥认证的问题的,我们需要一个权威的CA 机构。
    2. Alice 把自己的信息(姓名、组织,地址,电邮,网址等)和自己的公钥打包成一个 CSR 的文件,发给 CA 机构,
    3. CA 机构会来找 Alice 做物理世界的认证,如果通过后,就会用自己的机构私钥,把CSR 变成一个签名证书。
    4. Bob 同学拿到 Alice 的证书,用 CA 机构的公钥解密后,得到 Alice 的公钥
    5. 后面就可以签证 信息是否来自 Alice 了。

    是的,这个过程就是在“套娃”,这种证书机构还可以给下级的证书机构发证,于是就会一层套一层地,形成一个证书链,顶层的叫根证书,你得绝对信任之。对于验证证书真实性的客户端,它需要能够验证链中所有 CA 的签名,这意味着客户端需要访问链中所有 CA 的证书。

    证书生成过程演示

    并不是所有的场景都需要向这些大型的 CA 机构申请公钥证书,在任何一个企业,组织或是团体内都可以自己形这样的“小王国”,也就是说,你可以自行生成这样的证书,只需要你自己保证自己的生成证书的私钥的安全,以及不需要扩散到整个互联网。下面,我们用 openssl命令来演示这个过程。

    1)生成 CA 的证书(公钥) ca.crt 和私钥 ca.key

    openssl req -newkey rsa:2048 
        -new -nodes -x509 
        -days 365 
        -out ca.crt 
        -keyout ca.key 
        -subj "/C=SO/ST=Earth/L=Mountain/O=CoolShell/OU=HQ/CN=localhost"

    2)  生成 alice 的私钥

    openssl genrsa -out alice.key 2048

    3)生成 Alice 的 CSR – Certificate Signing Request

    openssl req -new -key alice.key 365 -out alice.csr 
        -subj "/C=CN/ST=Beijing/L=Haidian/O=CoolShell/OU=Test/CN=localhost.alice"

    4)使用 CA 给 Alice 签名证书

    openssl x509  -req -in alice.csr 
        -extfile <(printf "subjectAltName=DNS:localhost.alice")  
        -CA ca.crt -CAkey ca.key  
        -days 365 -sha256 -CAcreateserial 
        -out alice.crt

    双向认证 mTLS

    上面,我们说的基本上都是单向认证,大量的场景都是确保用户方访问的是真正的服务方,如:银行,电商网站,等。这样可以保证用户不会被钓鱼网站或是中间人攻击。但是,很多时候,我们也是需要双向认证的。下面是一个典型的场景——微信支付和商户间交互

    • 用户到商家那边买东西,商家要求用户进行支付。
    • 用户选择了微信支付,于是,界面从商户侧切到了微信侧
    • 微信那边支付完成后,商户这边收到微信那边支付完成的通知,于是开始发货。

    这个过程中有件事非常重要——就是微信通知商户支付完成的时候。

    • 微信得确保通知到的就是用户所支付商户,而不是别个。
    • 商户也得要能确认,来通知我的就是微信,不是别人。

    一般来说,微信会给商户一个 AppID和一个 AppSerct,用这个来确保是我认证过的商户来调用我,然后,需要商户在自己的系统里填一个回调的 URL,并通过平台设置的 key来做 MD5/HMAC的签名来确保是官方的回调。这都是在《HTTP API 认证授权术》中提到过的技术,是相对传统的技术。

    如今,mTLS是确保云原生应用程序中服务之间的通信安全的首选协议。 也就是双向认证。

    传统的 TLS 认证过程是:

    1. 客户端连接到服务器
    2. 服务器提供其 TLS 证书
    3. 客户端验证服务器的证书
    4. 客户端和服务器通过加密的 TLS 连接交换信息

    在 mTLS 中,客户端和服务器都有一个证书,双方都使用他们的公钥/私钥对进行身份验证。与常规 TLS 相比,mTLS 中有额外的步骤来验证双方(以粗体显示的额外步骤):

    1. 客户端连接到服务器
    2. 服务器提供其 TLS 证书
    3. 客户端验证服务器的证书
    4. 客户端出示其 TLS 证书
    5. 服务器验证客户端的证书
    6. 服务器授予访问权限
    7. 客户端和服务器通过加密的 TLS 连接交换信息

    mTLS 需要“根”TLS 证书;这我们自己来完成证书颁发机构的职责。授权客户端和服务器使用的证书必须与此根证书相对应。根证书是自签名的,这意味着我们需要自己创建它。(注:此方法不适用于公共 Internet 上的单向 TLS,因为外部证书颁发机构必须颁发这些证书)

    那么,为什么整个互联网上都用了 TLS 了,为什么 不升级一下使用 mTLS?这里有两方面的原因:

    • 公共互联网上要解决的问题是:A) 确保用户访问到的是正确的网站,而不是钓鱼网站。B)网站传输的内容是安全和私密且不会被篡改的。
    • 将 TLS 证书分发到所有最终用户设备将非常困难。生成、管理和验证为此所需的数十亿个证书几乎是不可能的任务。

    在较小的范围内,mTLS 对于单个组织非常有用且非常实用,尤其是当这些组织采用零信任方法来确保网络安全时。由于默认情况下零信任方法不信任任何用户、设备或请求,因此组织必须能够在每次尝试访问网络中的任何点时对每个用户、设备和请求进行身份验证。mTLS 通过对用户进行身份验证和设备验证来帮助实现这一目标。

    关于 mTLS,这里有一个我用 Golang 写的示例 – https://github.com/haoel/mTLS,大家可以参考一下。

    P.S. 本文图版中的卡司来自安全圈的标准 Cast,参看 Alice and Bob

    (全文完)

    (转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

    The post 网络数字身份认证术 first appeared on 酷 壳 – CoolShell.

  • 我做系统架构的一些原则

    我做系统架构的一些原则

    工作 20 多年了,这 20 来年看到了很多公司系统架构,也看到了很多问题,在跟这些公司进行交流和讨论的时候,包括进行实施和方案比较的时候,都有很多各种方案的比较和妥协,因为相关的经历越来越多,所以,逐渐形成了自己的逻辑和方法论。今天,想写下这篇文章,把我的这些个人的经验和想法总结下来,希望能够让更多的人可以参考和借鉴,并能够做出更好的架构来。另外,我的这些思维方式和原则都针对于现有市面上众多不合理的架构和方案,所以,也算是一种“纠正”……(注意,这篇文章所说的这些架构上的原则,一般适用于相对比较复杂的业务,如果只是一些简单和访问量不大的应用,那么你可能会得出相反的结论)

    原则一:关注于真正的收益而不是技术本身

    对于软件架构来说,我觉得第一重要的是架构的收益,如果不说收益,只是为了技术而技术,而没有任何意义。对于技术收益来说,我觉得下面这几个收益是非常重要的:

    • 是否可以降低技术门槛加快整个团队的开发流程。能够加快整个团队的工程流程,快速发布,是软件工程一直在解决的问题,所以,系统架构需要能够进行并行开发,并行上线和并行运维,而不会让某个团队成为瓶颈点。(注:就算拖累团队的原因是组织构架,也不妨碍我们做出并行的系统架构设计)
    • 是否可以让整个系统可以运行的更稳定。要让整个系统可以运行的更为的稳定,提升整个系统的 SLA,就需要对有计划和无计划的停机做相应的解决方案(参看《关于高可用的架构》)
    • 是否可以通过简化和自动化降低成本。最高优化的成本是人力成本,人的成本除了慢和贵,还有经常不断的 human error。如果不能降低人力成本,反而需要更多的人,那么这个架构设计一定是失败的。除此之外,是时间成本,资金成本。

    如果一个系统架构不能在上面三个事上起到作用,那就没有意义了。

    原则二:以应用服务和 API 为视角,而不是以资源和技术为视角

    国内很多公司都会有很多分工,基本上都会分成运维和开发,运维又会分成基础运维和应用运维,开发则会分成基础核心开发和业务开发。不同的分工会导致完全不同的视角和出发点。比如,基础运维和开发的同学更多的只是关注资源的利用率和性能,而应用运维和业务开发则更多关注的是应用和服务上的东西。这两者本来相关无事,但是因为分布式架构的演进,导致有一些系统已经说不清楚是基础层的还是应用层的了,比如像服务治理上的东西,里面即有底层基础技术,也需要业务的同学来配合,包括 k8s 也样,里面即有底层的如网络这样的技术,也有需要业务配合的 readniess和 liveness 这样的健康检查,以及业务应用需要 configMap 等等 ……

    这些东西都让我感觉到所谓 DevOps,其实就是因为很多技术和组件已经分不清是 Dev 还是 Ops 的了,所以,需要合并 Dev和 Ops。而且,整个组织和架构的优化,已经不能通过调优单一分工或是单一组件能够有很大提升的了。其需要有一种自顶向下的,整体规划,统一设计的方式,才能做到整体的提升(可以试想一下城市交通的优化,当城市规模到一定程度的时候,整体的性能你是无法通过优化几条路或是几条街区来完成的,你需要对整个城市做整体的功能体的规划才可能达到整体效率的提升)。而为了做到整体的提升,需要所有的人都要有一个统一的视角和目标,这几年来,我觉得这个目标就是——要站在服务和 对外API的视角来看问题,而不是技术和底层的角度。

    原则三:选择最主流和成熟的技术

    技术选型是一件很重要的事,技术一旦选错,那会导致整个架构需要做调整,而对架构的调整重来都不是一件简单的事,我在过去几年内,当系统越来越复杂的时候,用户把他们的  PHP,Python, .NET,或 Node.js 的架构完全都迁移到 Java + Go 的架构上来的案例不断的发生。这个过程还是非常痛苦的,但是你没有办法,当你的系统越来越复杂,越来越大时,你就再也不能在一些玩具技术上玩了,你需要的更为工业化的技术。

    • 尽可能的使用更为成熟更为工业化的技术栈,而不是自己熟悉的技术栈。 所谓工业化的技术栈,你可以看看大多数公司使用的技术栈,比如:互联网,金融,电信……等等 ,大公司会有更多的技术投入,也需要更大规模的生产,所以,他们使用的技术通常来说都是比较工业化的。在技术选型上,千万不要被——“你看某个视频公司也在用这个技术”,或是一些在论坛上看到的一些程序员吐槽技术的观点(没有任何的数据,只有自己的喜好)来决定自己的技术,还是看看主流大多数公司实际在用的技术栈,会更靠谱一些。
    • 选择全球流行的技术,而不是中国流行的技术。技术这个东西一定是一个全球化的东西,不是一个局域化的事。所以,一定要选国际化的会更好。另外,千万不要被某些公司的“特别案例”骗过去了,那怕这个案例很性感,关键还是要看解决问题的思路和采用的技术是否具有普世性。只有普世性的技术有更强的生命力。
    • 尽可能的使用红利大的主流技术,而不要自己发明轮子,更不要魔改。我见过好些个公司魔改开源软件,比如有个公司同魔改mesos,最后改着改着发现自己发明另一个 kubernetes。我还见过很多公司或技术团队喜欢自己发明自己的专用轮子,最后都会被主流开源软件所取代。完全没有必要。不重新发明轮子,不魔改,不是因为自己技术不能,而是因为,这个世界早已不是自己干所有事的年代了,这个时代是要想尽方法跟整个产业,整个技术社区融合和合作,这样才会有最大的收益。那些试图因为某个特例需要自成一套的玩法,短期没问题,但长期来说,我都不看好。
    • 绝大多数情况下,如无非常特殊要求,选 Java基本是不会错的。一方面,这是因为 Java 的业务开发的生产力是非常好的,而且有 Spring 框架保障,代码很难写烂,另外,Java 的社区太成熟了,你需要的各种架构和技术都可以很容易获得,技术红利实在是太大。这种运行在JVM上的语言有太多太多的好处了。在 Java 的技术栈上,你的架构风险和架构的成本(无论是人力成本,时间成本和资金成本)从长期来说都是最优的

    在我见过的公司中,好些公司的架构都被技术负责人个人的喜好、擅长和个人经验给绑架了,完全不是从一个客观的角度来进行技术选型。其实,从 0 到 1 的阶段,你用什么样的技术都行,如果你做一个简单的应用,没有事务处理没有复杂的交易流程,比如一些论坛、社交之类的应用,你用任何语言都行。但是如果有一天你的系统变复杂了,需要处理交易了,量也上来了,从 1 到 10,甚至从 10 到 100,你的开发团队也变大了,需要构建的系统越来越大,你可能会发现你只有一个选择,就是 Java。想想京东从.NET 到 Java,淘宝从 PHP 到 Java……

    注,一些有主观喜好的人一定会对我上述对 Java 的描述感到不适,我还用一些证据说明一下——全中国所有的电商平台,几百家银行,三大电信运营商,所有的保险公司,劵商的系统,医院里的系统,电子政府系统,等等,基本都是用 Java 开发的,包括 AWS 的主流语言也是 Java,阿里云一开始用 C++/Python 写控制系统,后面也开始用 Java ……你可能会说 B站是用 go语言,但是你可能不知道 B 站的电商和大数据是用 Java……懂着数据分析的同学,建议上各大招聘网站上搜一下 Java 的职位数量,你就知道某个技术是否主流和热门……

    原则四:完备性会比性能更重要

    我发现好些公司的架构师做架构的时候,首要考虑的是架构的性能是否能够撑得住多大多大的流量,而不是考虑系统的完备性和扩展性。所以,我已经多次见过这样的案例了,一开始直接使用 MongoDB 这样的非关系型数据库,或是把数据直接放在 Redis 里,而直接放弃关系型数据库的数据完备性的模型,而在后来需要在数据上进行关系查询的时候,发现 NoSQL 的数据库在 Join 上都表现的太差,然后就开始各种飞线,为了不做 Join 就开始冗余数据,然而自己又维护不好冗余数据后带来的数据一致性的问题,导致数据上的各种错乱丢失。

    所以,我给如下的一些如下的架构原则:

    • 使用最科学严谨的技术模型为主,并以不严谨的模型作为补充。对于上面那个案例来说,就是——永远使用完备支持 ACID 的关系型数据库,然后用 NoSQL 作补充,而不是完全放弃关系型数据库。这里的原则就是所谓的“先紧后松”,一开始紧了,你可以慢慢松,但是开始松了,以后你想紧再也紧不过来了。
    • 性能上的东西,总是有很多解的。我这么多年的经历告诉我,性能上的事,总是有解的,手段也是最多的,这个比起架构的完备性和扩展性来说真的不必太过担心。

    为了追求所谓的性能,把整个系统的完备性丢失掉,相当地得不偿失。

    原则五:制定并遵循服从标准、规范和最佳实践

    这个原则是非常重要的,因为只有服从了标准,你的架构才能够有更好的扩展性。比如:我经常性的见到很多公司的系统既没有服从业界标准,也没有形成自己公司的标准,感觉就像一群乌合之众一样。最典型的例子就是 HTTP 调用的状态返回码。业内给你的标准是 200表示成功,3xx 跳转,4xx 表示调用端出错,5xx 表示服务端出错,我实在是不明白为什么无论成功和失败大家都喜欢返回 200,然后在 body 里指出是否error(前两年我在微信公众号里看到一个有一定名气的互联网老兵推荐使用无论正确还是出错都返回 200 的做法,我在后台再三确认后,我发现这样的架构师真是害人不浅)。这样做最大的问题是——监控系统将在一种低效的状态下工作。监控系统需要把所有的网络请求包打开后才知道是否是错误,而且完全不知道是调用端出错还是服务端出错,于是一些像重试或熔断这样的控制系统完全不知道怎么搞(如果是 4xx错,那么重试或熔断是没有意义的,只有 5xx 才有意义)。有时候,我会有种越活越退步的感觉,错误码设计这种最基本最基础的东西为什么会没有?并且一个公司会任由着大家乱来?这些基础技能怎么就这样丢掉了?

    还有,我还见过一些公司,他们整个组织没有一个统一的用户 ID 的设计,各个系统之间同步用户的数据是通过用户的身份证 ID,是的,就是现实世界的身份证 ID,包括在网关上设置的用户白名单居然也是用身份证 ID。我对这个公司的内的用户隐私管理有很大的担忧。一个企业,一个组织,如果没有标准和规范,也就会有抽象,这一定是要出各种乱子的。

    下面,我罗列一些你需要注意的标准和规范(包括但不限于):

    • 服务间调用的协议标准和规范。这其中包括 Restful API路径, HTTP 方法、状态码、标准头、自定义头等,返回数据 JSon Scheme……等。
    • 一些命名的标准和规范。这其中包括如:用户 ID,服务名、标签名、状态名、错误码、消息、数据库……等等
    • 日志和监控的规范。这其中包括:日志格式,监控数据,采样要求,报警……等等
    • 配置上的规范。这其中包括:操作系统配置、中间件配置,软件包……等等
    • 中间件使用的规范。数据库,缓存、消息队列……等等
    • 软件和开发库版本统一。整个组织架构内,软件或开发库的版本最好每年都升一次级,然后在各团队内统一。

    这里重要说一下两个事:

    • Restful API 的规范。我觉得是非常重要的,这里给两个我觉得写得最好的参考:PaypalMicrosoft 。Restful API 有一个标准和规范最大的好处就是监视可以很容易地做各种统计分析,控制系统可以很容易的做流量编排和调度。
    • 另一个是服务调用链追踪。对于服务调用链追踪来说,基本上都是参考于 Google Dapper 这篇论文,目前有很多的实现,最严格的实现是 Zipkin,这也是 Spring Cloud Sleuth 的底层实现。Zipkin 贴近 Google Dapper 论文的好处在于——无状态,快速地把 Span 发出来,不消耗服务应用侧的内存和 CPU。这意味着,监控系统宁可自己死了也不能干扰实际应用。
    • 软件升级。我发现很多公司包括 BAT,他们完全没有软件升级的活动,全靠开发人员自发。然而,这种成体系的活动,是永远不可能靠大众的自发形成的。一个公司至少一年要有一次软件版本升级的review,然后形成软件版本的统一和一致,这样会极太简化系统架构的复杂度。

    原则六:重视架构扩展性和可运维性

    在我见过很多架构里,技术人员只考虑当下,但从来不考虑系统的未来扩展性和可运维性。所谓的管生不管养。如果你生下来的孩子胳膊少腿,严重畸形,那么未来是很难玩的。因为架构和软件不是写好就完的,是需要不断修改不断维护的,80%的软件成本都是在维护上。所以,如何让你的架构有更好的扩展性,可以更容易地运维,这个是比较重要的。所谓的扩展性,意味着,我可以很容易地加更多的功能,或是加入更多的系统,而所谓可运维,就是说我可以对线上的系统做任意的变更。扩展性要求的是有标准规范且不耦合的业务架构,可运维性要求的则是可控的能力,也就是一组各式各样的控制系统。

    • 通过服务编排架构来降低服务间的耦合。比如:通过一个业务流程的专用服务,或是像 Workflow,Event Driven Architecture , Broker,Gateway,Service Discovery 等这类的的中间件来降低服务间的依赖关系。
    • 通过服务发现或服务网关来降低服务依赖所带来的运维复杂度。服务发现可以很好的降低相关依赖服务的运维复杂度,让你可以很轻松的上线或下线服务,或是进行服务伸缩。
    • 一定要使用各种软件设计的原则。比如:像SOLID这样的原则(参看《一些软件设计的原则》),IoC/DIP,SOA 或 Spring Cloud 等 架构的最佳实践(参看《SteveY对Amazon和Google平台的吐槽》中的 Service Interface 的那几条军规),分布式系统架构的相关实践(参看:《分布式系统的事务处理》,或微软件的 《Cloud Design Patterns》)……等等

    原则七:对控制逻辑进行全面收口

    所有的程序都会有两种逻辑,一种是业务逻辑,一种是控制逻辑,业务逻辑就是完成业务的逻辑,控制逻辑是辅助,比如你用多线程,还是用分布式,是用数据库还是用文件,如何配置、部署,运维、监控,事务控制,服务发现,弹性伸缩,灰度发布,高并发,等等,等等 ……这些都是控制逻辑,跟业务逻辑没有一毛钱关系。控制逻辑的技术深度会通常会比业务逻辑要深一些,门槛也会要高一些,所以,最好要专业的程序员来负责控制逻辑的开发,统一规划统一管理,进行收口。这其中包括:

    • 流量收口。包括南北向和东西向的流量的调度,主要通过流量网关,开发框架 SDK或 Service Mesh 这样的技术。
    • 服务治理收口。包括:服务发现、健康检查,配置管理、事务、事件、重试、熔断、限流……主要通过开发框架 SDK – 如:Spring Cloud,或服务网格Service Mesh等技术。
    • 监控数据收口。包括:日志、指标、调用链……主要通过一些标准主流的探针,再加上后台的数据清洗和数据存储来完成,最好是使用无侵入式的技术。监控的数据必须统一在一个地方进行关联,这样才会产生信息。
    • 资源调度有应用部署的收口。包括:计算、网络和存储的收口,主要是通过容器化的方案,如k8s来完成。
    • 中间件的收口。包括:数据库,消息,缓存,服务发现,网关……等等。这类的收口方式一般要在企业内部统一建立一个共享的云化的中间件资源池。

    对此,这里的原则是:

    • 你要选择容易进行业务逻辑和控制逻辑分离的技术。这里,Java 的 JVM+字节码注入+AOP 式的Spring 开发框架,会带给你太多的优势。
    • 你要选择可以享受“前人种树,后人乘凉”的有技术红利的技术。如:有庞大社区而且相互兼容的技术,如:Java, Docker,  Ansible,HTTP,Telegraf/Collectd……
    • 中间件你要使用可以 支持HA集群和多租户的技术。这里基本上所有的主流中间件都会支持 HA 集群方式的。

    原则八:不要迁就老旧系统的技术债务

    我发现很多公司都很非常大的技术债务,这些债务具体表现如下:

    • 使用老旧的技术。比如,使用HTTP1.0, Java 1.6,Websphere,ESB,基于 socket的通讯协议,过时的模型……等等
    • 不合理的设计。比如,在 gateway 中写大量的业务逻辑,单体架构,数据和业务逻辑深度耦合,错误的系统架构(把缓存当数据库,用消息队列同步数据)……等等
    •  缺少配套设施。比如,没有自动化测试,没有好的软件文档,没有质量好的代码,没有标准和规范……等等

    来找我寻求技术帮助的人都有各种各样的问题。我都会对他们苦口婆心地说同样的一句话——“如果你是来找我 case-by-case 解决问题,我兴趣不大,因为,你们千万不要寄希望能够很简单的把一辆夏利车改成一辆法拉利跑车,或是把一栋地基没打好的歪楼搞正。以前欠下的技术债,都得要还,没打好的地基要重新打,没建配套设施都要建。这些基础设施如果不按照正确科学的方式建立的话,你是不可能有一个好的的系统,我也没办法帮你 case-by-case 的解决问题……”,一开始,他们都会对我说,没问题,我们就是要还债,但是,最后发现要还的债真多,有点承受不了,就开始现原形了。

    他们开始为自己的“欠的技术债”找各种合理化的理由——给你解释各种各样的历史原因和不得以而为之的理由。谈着谈着,让我有一种感觉——他们希望得到一种什么都不改什么都不付出的方式就可以进步的心态,他们宁可让新的技术 low 下来迁就于这些技术债,把新的技术滥用地乱七八糟的。有一个公司,他们的系统架构和技术选型基本都搞错了,使用错误的模型构建系统,导致整个系统的性能非常之差,也才几千万条数据,但他们想的不是还债,不是把地基和配套设施建好,而且要把楼修的更高,上更多的系统——他们觉得现有的系统挺好,性能问题的原因是他们没一个大数据平台,所以要建大数据平台……

    我见过很多很多公司,包括大如 BAT 这样的公司,都会在原来的技术债上进行更多的建设,然后,技术债越来越大,利息越来越大,最终成为一个高利贷,再也还不了(我在《开发团队的效率》一文中讲过一个 WatchDog 的架构模式,一个系统烂了,不是去改这个系统,而是在旁边建一个系统来看着它,我很难理解为什么会有这样的逻辑,也许是为了要解决更多的就业……)

    这里有几个原则和方法我是非常坚持的,分享给大家:

    • 与其花大力气迁就技术债务,不如直接还技术债。是所谓的长痛不如短痛。
    • 建设没有技术债的“新城区”,并通过“防腐层 ”的架构模型,不要让技术债侵入“新城区”

    原则九:不要依赖自己的经验,要依赖于数据和学习

    有好些人来找我跟我说他们的技术问题,然后希望我能够给他们一个答案。我说,我需要了解一下你现有系统的情况,也就是需要先做个诊断,我只有得到这些数据后,我才可能明白真正的原因是什么 ,我才可能给你做出一个比较好的技术方案。我个人觉得这是一种对对方负责的方法,因为技术手段太多了,所有的技术手段都有适应的场景,并且有各种 trade-off,所以,只有调研完后才能做出决定。这跟医生看病是一样的,确诊病因不能靠经验,还是要靠诊断数据。在科学面前,所有的经验都是靠不住的……

    另外,如果有一天你在做技术决定的时候,开始凭自己以往的经验,那么你就已经不可能再成长了。人都是不可能通过不断重复过去而进步的,人的进步从来都是通过学习自己不知道的东西。所以,千万不要依赖于自己的经验做决定。做任何决定之前,最好花上一点时间,上网查一下相关的资料,技术博客,文章,论文等 ,同时,也看看各个公司,或是各个开源软件他们是怎么做的?然后,比较多种方案的 Pros/Cons,最终形成自己的决定,这样,才可能做出一个更好的决定。

    原则十:千万要小心 X – Y 问题,要追问原始需求

    对于 X-Y 问题,也就是说,用户为了解决 X问题,他觉得用 Y 可以解,于是问我 Y 怎么搞,结果搞到最后,发现原来要解决的 X 问题,这个时候最好的解决方案不是 Y,而是 Z。 这种 X-Y 问题真是相当之多,见的太多太多了。所以,每次用户来找我的时候,我都要不断地追问什么是 X 问题。

    比如,好些用户都会来问我他们要一个大数据流式处理,结果追问具体要解决什么样的问题时,才发现他们的问题是因为服务中有大量的状态,需要把相同用户的数据请求放在同一个服务上处理,而且设计上导致一个慢函数拖慢整个应用服务。最终就是做一下性能调优就好了,根本没有必要上什么大数据的流式处理。

    我很喜欢追问为什么 ,这种追问,会让客户也跟着来一起重新思考。比如,有个客户来找我评估的一个技术架构的决定,从理论上来说,好像这个架构在用户的这个场景下非常不错。但是,这个场景和这个架构是我职业生涯从来没有见过的。于是,我开始追问这个为什么会是这么一个场景?当我追问的时候,我发现用户都感到这个场景的各种不合理。最后引起了大家非常深刻的研讨,最终用户把那个场景修正后,而架构就突然就变成了一个常见且成熟的的模型……

    原则十一:激进胜于保守,创新与实用并不冲突

    我对技术的态度是比较激进的,但是,所谓的激进并不是瞎搞,也不是见新技术就上,而是积极拥抱会改变未来的新技术,如:Docker/Go,我就非常快地跟进,但是像区块链或是 Rust 这样的,我就不是很积极。因为,其并没有命中我认为的技术趋势的几个特征(参看《Go,Docker 和新技术 》)。当然,我也不是不喜欢的就不学了,我对区块链和 Rust 我一样学习,我也知道这些技术的优势,但我不会大规模使用它们。另外,我也尊重保守的决定,这里面没有对和错。但是,我个人觉得对技术激进的态度比起保守来说有太多的好处了。一方面来说,对于用户来说,很大程度上来说,新技术通常都表面有很好的竞争力,而且我见太多这样成功的公司都在积极拥抱新的技术的,而保守的通常来说都越来越不好。

    有一些人会跟我说,我们是实用主义,我们不需要创新,能解决当下的问题就好,所以,我们不需要新技术,现有的技术用好就行了。这类的公司,他们的技术设计第一天就在负债,虽然可以解决当下问题,但是马上就会出现新的问题,然后他们会疲于解决各种问题。最后呢,最后还是会走到新的技术上。

    这里的逻辑很简单 —— 进步永远来自于探索,探索是要付出代价的,但是收益更大。对我而言,不敢冒险才是最大的冒险,不敢犯错才是最大的错误,害怕失去会让你失去的更多……

    (全文完)

    (转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

    The post 我做系统架构的一些原则 first appeared on 酷 壳 – CoolShell.

  • ETCD的内存问题

    今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为什么要用etcd,使用etcd的用户场景,包括etcd的一些导致内存占用比较大的设计,以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题,还能让你有更多的收获。当然,也欢迎您关注我们的开源软件,给我们一些鼓励。

    为什么要用ETCD

    先说一下为什么要用etcd。先从一个我们自己做的一个API网关 – Easegress(源码)说起。

    Easegress 是我们开发并开源的一个API应用网关产品,这个API应用网关不仅仅只是像nginx那样用来做一个反向代理,这个网关可以做的事很多,比如:API编排、服务发现、弹力设计(熔断、限流、重试等)、认证鉴权(JWT,OAuth2,HMAC等)、同样支持各种Cloud Native的架构如:微服务架构,Service Mesh,Serverless/FaaS的集成,并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以,为了达到这些目标,在2017年的时候,我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的,必需重新写一个(后来其他人也应该跟我们的想法一样,所以,Lyft写了一个Envoy。只不过,Envoy是用C++写的,而我用了技术门槛更低的Go语言)

    另外,Easegress最核心的设计主要有三个:

    • 一是无第三方依赖的自己选主组集群的能力
    • 二是像Linux管道命令行那样pipeline式的插件流式处理(支持Go/WebAssembly)
    • 三是内置一个Data Store用于集群控制和数据共享。

    对于任何一个分布式系统,都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制,并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据,以保证整个集群的行为是统一一致的。如果没有这么一个东西的话,就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意,Zookeeper他们主要不是给你存数据的,而是给你组集群的。

    Zookeeper是一个很流行的开源软件,也被用于各大公司的生产线,包括一些开源软件,比如:Kafka。但是,这会让其它软件有一个依赖,并且在运维上带来很大的复杂度。所以,Kafka在最新的版本也通过内置了选主的算法,而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力,也是kubernetes组建集群的关键组件。Easegress在一开始(5年前)使用了gossip协议同步状态(当时想的过于超前,想做广域网的集群),但是后发现这个协议太过于复杂,而且很难调试,而广域网的API Gateway也没遇到相应的场景。所以,在3年前的时候,为了稳定性的考量,我们把其换成了内嵌版本的etcd,这个设计一直沿用到今天。

    Easegress会把所有的配置信息都放到etcd里,还包括一些统计监控数据,以及一些用户的自定义数据(这样用户自己的plugin不但可以在一条pipeline内,还可以在整个集群内共享数据),这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标,尤其是开源软件更要想方设法降低技术门槛让技术易扩展,这就是为什么Google的很多开源软件都会选使用Go语言的原因,也是为什么Go正在取代C/C++的做PaaS基础组件的原因。

    背景问题

    好了,在介绍完为什么要用etcd以后,我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候,在Easegress内配置了上千条pipeline,导致 Easegress的内存飙升的非常厉害- 10+GB 以上,而且长时间还下不来。

    用户报告的问题是——

    在Easegress 1.4.1 上创建一个HTTP对象,1000个Pipeline,在Easegres初始化启动完成时的内存占用大概为400M,运行80分钟后2GB,运行200分钟后达到了4GB,这期间什么也没有干,对Easegress没有进行过一次请求。

    一般来说,就算是API再多也不应该配置这么多的处理管道pipeline的,通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的,就像nginx下的location的配置,一般来说不会太多的。但是,在用户的这个场景下配置了上千个pipeline,我们也是头一次见,应该是用户想做更细粒度的控制。

    经过调查后,我们发现内存使用基本全部来自etcd,我们实在没有想到,因为我们往etcd里放的数据也没有多少个key,感觉不会超过10M,但不知道为什么会占用了10GB的内存。这种时候,一般会怀疑etcd有内存泄漏,上etcd上的github上搜了一下,发现etcd在3.2和3.3的版本上都有内存泄露的问题,但都修改了,而 Easegress 使用的是3.5的最新版本,另外,一般来说内存泄漏的问题不会是这么大的,我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd,那么只有一条路了,沉下心来,把etcd的设计好好地看一遍。

    大概花了两天左右的时间看了一下etcd的设计,我发现了etcd有下面这些消耗内存的设计,老实说,还是非常昂贵的,这里分享出来,避免后面的同学再次掉坑。

    首当其冲是——RaftLog。etcd用Raft Log,主要是用于帮助follower同步数据,这个log的底层实现不是文件,而是内存。所以,而且还至少要保留 5000 条最新的请求。如果key的size很大,这 5000条就会产生大量的内存开销。比如,不断更新一个 1M的key,哪怕是同一个key,这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过  issue #12548 ,不过,这个问题不了了之了。这个5000还是一个hardcode,无法改。(参看 DefaultSnapshotCatchUpEntries 相关源码

    // DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
    // to catch-up after compacting the raft storage entries.
    // We expect the follower has a millisecond level latency with the leader.
    // The max throughput is around 10K. Keep a 5K entries is enough for helping
    // follower to catch up.
    DefaultSnapshotCatchUpEntries uint64 = 5000

    另外,我们还发现,这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000,我们估计etcd官方团队也意识到10000有点太耗内存了,所以,降了一半,但是又怕follwer同步不上,所以,保留了 5000条……(在这里,我个人感觉还有更好的方法,至少不用全放在内存里吧……)

    另外还有下面几项也会导致etcd的内存会增加

    1. 索引。etcd的每一对 key-value 都会在内存中有一个 B-tree 索引。这个索引的开销跟key的长度有关,etcd还会保存版本。所以B-tree的内存跟key的长度以及历史版本号数量也有关系。
    2. mmap。还有,etcd 使用 mmap 这样上古的unix技术做文件映射,会把他的blotdb的内存map到虚拟内存中,所以,db-size越大,内存越大。
    3. Watcher。watch也会占用很大的内存,如果watch很多,连接数多,都会堆积内存。

    (很明显,etcd这么做就是为了一个高性能的考虑)

    Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因,对于索引和mmap,使用 etcd 的 compact 和 defreg (压缩和碎片整理应该可以降低内存,但用户那边不应该是这个问题的核心原因)。

    针对用户的问题,大约有1000多条pipeline,因为Easegress会对每一条pipeline进行数据统计(如:M1, M5, M15, P99, P90, P50等这样的统计数据),统计信息可能会有1KB-2KB左右,但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中,这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key,而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景,所以,这个内存问题没有暴露出来。

    于是,我们最终的解决方案也很简单,我们修改我们的策略,不再写这么大的Value的数据了,虽然以前只写在一个key上,但是Key的值太大,现在把这个大Key值拆分成多个小的key来写,这样,实际保存的数据没有发生变化,但是RaftLog的每条数据量就小了,所以,以前是5000条 2M(10GB),现在是5000条 1K(500MB),就这样解决了这个问题。相关的PR在这里 PR#542

    总结

    要用好 etcd,有如下的实践

    • 避免大尺寸的key和value,一方面会通过一个内存级的 Raft Log 占大量内存,另一方面,B-tree的多版本索引也会因为这样耗内存。
    • 避免DB的尺寸太大,并通过 compact和defreg来压缩和碎片整理降低内存。
    • 避免大量的Watch Client 和 Watch数。这个开销也是比较大的。
    • 最后还有一个,就是尽可能使用新的版本,无论是go语言还是etcd,这样会少很多内存问题。比如:golang的这个跟LInux内核心相关的内存问题 —— golang 1.12的版sget的是 MADV_FREE 的内存回收机制,而在1.16的时候,改成了 MADV_DONTNEED ,这两者的差别是,FREE表示,虽然进程标记内存不要了,但是操作系统会保留之,直到需要更多的内存,而 DONTNEED 则是立马回收,你可以看到,在常驻内存RSS 上,前者虽然在golang的进程上回收了内存,但是RSS值不变,而后者会看到RSS直立马变化。Linux下对 MADV_FREE 的实现在某些情况下有一定的问题,所以,在go 1.16的时候,默认值改成了 MADV_DONTNEED 。而 etcd 3.4 是用 来1.12 编译的。

    最后,欢迎大家关注我们的开源软件! https://github.com/megaease/ 

    (全文完)

    (转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

    The post ETCD的内存问题 first appeared on 酷 壳 – CoolShell.

  • “一把梭:REST API 全用 POST”

    “一把梭:REST API 全用 POST”

    写这篇文章的原因主要还是因为V2EX上的这个贴子,这个贴子中说——

    “对接同事的接口,他定义的所有接口都是 post 请求,理由是 https 用 post 更安全,之前习惯使用 restful api ,如果说 https 只有 post 请求是安全的话?那为啥还需要 get 、put 、delete ?我该如何反驳他。”

    然后该贴中大量的回复大概有这么几种论调,1)POST挺好的,就应该这么干,沟通少,2)一把梭,早点干完早点回家,3)吵赢了又怎么样?工作而已,优雅不能当饭吃。虽然评论没有一边倒,但是也有大量的人支持。然后,我在Twitter上嘲讽了一下,用POST干一切就像看到了来你家装修工人说,“老子干活就是用钉子钉一切,什么螺丝、螺栓、卡扣、插销……通通不用,钉枪一把梭,方便,快捷,安全,干完早回家……不过,还是有一些网友觉得用POST挺好的,而且可以节约时间。所以,正好,我在《我做系统架构的原则》中的“原则五”中反对API返回码无论对错全是200的返回那,我专门写下这一篇文章,以正视听。

    这篇文章主要分成下面这几个部分:

    1. 为什么要用不同的HTTP动词?
    2. Restful 进行复杂查询
    3. 几个主要问题的回应
      • POST 更安全吗?
      • 全用 POST 可以节省时间沟通少吗?
      • 早点回家的正确姿势
      • 工作而已,优雅不能当饭吃

    为什么要用不同的HTTP动词

    编程世界通常来说有两种逻辑:“业务逻辑” 和 “控制逻辑”。

    • 业务逻辑。就是你实现业务需求的功能的代码,就是跟用户需求强相关的代码。比如,把用户提交的数据保存起来,查询用户的数据,完成一个订单交易,为用户退款……等等,这些是业务逻辑
    • 控制逻辑。就是我们用于控制程序运行的非功能性的代码。比如,用于控制程序循环的变量和条件,使用多线程或分布式的技术,使用HTTP/TCP协议,使用什么样数据库,什么样的中间件……等等,这些跟用户需求完全没关系的东西。

    网络协议也是一样的,一般来说,几乎所有的主流网络协议都有两个部分,一个是协议头,一个是协议体。协议头中是协议自己要用的数据,协议体才是用户的数据。所以,协议头主要是用于协议的控制逻辑,而协议体则是业务逻辑。

    HTTP的动词(或是Method)是在协议头中,所以,其主要用于控制逻辑。

    下面是HTTP的动词规范,一般来说,REST API 需要开发人员严格遵循下面的标准规范(参看RFC7231 章节4.2.2 – Idempotent Methods

    方法 描述 幂等
    GET 用于查询操作,对应于数据库的 select 操作
    PUT 用于所有的信息更新,对应于数据库的 update 操作 ︎︎
    DELETE 用于更新操作,对应于数据库的 delete 操作 ︎︎
    POST 用于新增操作,对应于数据库的 insert 操作
    HEAD 用于返回一个资源对象的“元数据”,或是用于探测API是否健康
    PATCH 用于局部信息的更新,对应于数据库的 update 操作
    OPTIONS 获取API的相关的信息。

    其中,PUT 和 PACTH 都是更新业务资源信息,如果资源对象不存在则可以新建一个,但他们两者的区别是,PUT 用于更新一个业务对象的所有完整信息,就像是我们通过表单提交所有的数据,而 PACTH 则对更为API化的数据更新操作,只需要更需要更新的字段(参看 RFC 5789 )。

    当然,现实世界中,可能并不一定严格地按照数据库操作的CRUD来理解API,比如,你有一个登录的API /login 你觉得这个API应该是 GETPOSTPUT 还是 PATCH ?登录的时候用户需要输入用户名和密码,然后跟数据库里的对比(select操作)后反回一个登录的session token,然后这个token作为用户登录的状态令牌。如果按上面表格来说,应该是 select 操作进行 GET ,但是从语义上来说,登录并不是查询信息,应该是用户状态的更新或是新增操作(新增session),所以还是应该使用 POST,而 /logout 你可以使用 DELETE这里相说明一下,不要机械地通过数据库的CRUD来对应这些动词,很多时候,还是要分析一下业务语义。

    另外,我们注意到,在这个表格的最后一列中加入了“是否幂等”的,API的幂等对于控制逻辑来说是一件很重要的事。所谓幂等,就是该API执行多次和执行一次的结果是完全一样的,没有副作用。

    • POST 用于新增加数据,比如,新增一个交易订单,这肯定不能是幂等的
    • DELETE 用于删除数据,一个数据删除多次和删除一次的结果是一样的,所以,是幂等的
    • PUT 用于全部数更新,所以,是幂等的。
    • PATCH用于局部更新,比如,更新某个字段 cnt = cnt+1,明显不可能是幂等操作。

    幂等这个特性对于远程调用是一件非常关键的事,就是说,远程调用有很多时候会因为网络原因导致调用timeout,对于timeout的请求,我们是无法知道服务端是否已经是收到请求并执行了,此时,我们不能贸然重试请求,对于不是幂等的调用来说,这会是灾难性的。比如像转帐这样的业务逻辑,转一次和转多次结果是不一样的,如果重新的话有可能就会多转了一次。所以,这个时候,如果你的API遵从了HTTP动词的规范,那么你写起程序来就可以明白在哪些动词下可以重试,而在哪些动词下不能重试。如果你把所有的API都用POST来表达的话,就完全失控了。

    除了幂等这样的控制逻辑之外,你可能还会有如下的这些控制逻辑的需求:

    • 缓存。通过CDN或是网关对API进行缓存,很显然,我们要在查询GET 操作上建议缓存。
    • 流控。你可以通过HTTP的动词进行更粒度的流控,比如:限制API的请用频率,在读操作上和写操作上应该是不一样的。
    • 路由。比如:写请求路由到写服务上,读请求路由到读服务上。
    • 权限。可以获得更细粒度的权限控制和审计。
    • 监控。因为不同的方法的API的性能都不一样,所以,可以区分做性能分析。
    • 压测。当你需要压力测试API时,如果没有动词的区分的话,我相信你的压力测试很难搞吧。
    • ……等等

    也许,你会说,我的业务太简单了,没有必要搞这么复杂。OK,没有问题,但是我觉得你最差的情况下,也是需要做到“读写分离”的,就是说,至少要有两个动词,GET 表示是读操作,POST表示是写操作。

    Restful 复杂查询

    一般来说,对于查询类的API,主要就是要完成四种操作:排序,过滤,搜索,分页。下面是一些相关的规范。参考于两个我觉得写的最好的Restful API的规范文档,Microsoft REST API GuidelinesPaypal API Design Guidelines

    • 排序。对于结果集的排序,使用 sort 关键字,以及 {field_name}|{asc|desc},{field_name}|{asc|desc} 的相关语法。比如,某API需要返回公司的列表,并按照某些字段排序,如:GET /admin/companies?sort=rank|asc 或是 GET /admin/companies?sort=rank|asc,zip_code|desc

    • 过滤。对于结果集的过滤,使用 filter 关键字,以及 {field_name} op{value} 的语法。比如: GET /companies?category=banking&location=china 。但是,有些时候,我们需要更为灵活的表达式,我们就需要在URL上构造我们的表达式。这里需要定义六个比较操作:=<><=>=,以及三个逻辑操作:andornot。(表达式中的一些特殊字符需要做一定的转义,比如:>= 转成 ge)于是,我们就会有如下的查询表达式:GET /products?$filter=name eq 'Milk' and price lt 2.55 查找所有的价柗小于2.55的牛奶。

    • 搜索。对于相关的搜索,使用 search 关键字,以及关键词。如:GET /books/search?description=algorithm 或是直接就是全文搜索 GET /books/search?key=algorithm

    • 分页。对于结果集进行分页处理,分页必需是一个默认行为,这样不会产生大量的返回数据。

      • 使用pageper_page代表页码和每页数据量,比如:GET /books?page=3&per_page=20
      • 可选。上面提到的page方式为使用相对位置来获取数据,可能会存在两个问题:性能(大数据量)与数据偏差(高频更新)。此时可以使用绝对位置来获取数据:事先记录下当前已获取数据里最后一条数据的ID时间等信息,以此获取 “该ID之前的数据” 或 “该时刻之前的数据”。示例:GET /news?max_id=23454345&per_page=20 或 GET /news?published_before=2011-01-01T00:00:00Z&per_page=20

    注意:这里需要注意一下,在理论上来说GET是可以带 body 的,但是很多程序的类库或是中间件并不支持 GET 带 body,导致你只能用 POST 来传递参数。这里的原则是:

    1. 对于简单的查询,很多参数都设计在 restful API 的路径上了,而 filter/sort/pagination 也不会带来很多的复杂,所以应该使用 GET 

    2. 对于复杂的查询来说,可能会有很复杂的查询参数,比如:ElasticSearch 上的 index/_search里的 DSL,你也应该尽可能的使用 GET,而不是POST 除非客观条件上不支持GET。ElasticSearch 的官方文档里也是这么说的。

    The authors of Elasticsearch prefer using GET for a search request because they feel that it describes the action—​retrieving information—​better than the POST verb. (我们推荐使用 GET而不是 POST,因为语义更清楚)However, because GET with a request body is not universally supported, the search API also accepts POST requests (除非你的类库或是服务器不支持 GET带参数 ,你再用POST,我们两个都支持)

    陈皓注:但是在 ElasticSearch 7.11 后,GET 也不支持 body 了。这是 ElasticSearch 的设计和实现不对应了。

    另外,对于一些更为复杂的操作,建议通过分别调用多个API的方式来完成,虽然这样会增加网络请求的次数,但是这样的可以让后端程序和数据耦合度更小,更容易成为微服务的架构。

    最后,如果你想在Rest中使用像GraphQL那样的查询语言,你可以考虑一下类似 OData 的解决方案。OData 是 Open Data Protocol 的缩写,最初由 Microsoft 于 2007 年开发。它是一种开放协议,使您能够以简单和标准的方式创建和使用可查询和可互操作的 RESTful API。

    几个主要问题的回应

    下面是对几个问题的直接回应,如果大家需要我回应更多的问题,可以在后面留言,我会把问题和我的回应添加到下面。

    1)为什么API 要Restful,并符合规范?

    Restful API算是一个HTTP的规范和标准了,你要说是最佳实践也好,总之,它是一个全世界对HTTP API的一个共识。在这个共识上,你可以无成本地享受很多的技术红利,比如:CDN,API网关,服务治理,监控……等等。这些都是可以让你大幅度降低研发成本,避免踩坑的原因。

    2)为什么“过早优化”不适用于API设计?

    因为API是一种契约,一旦被使用上,就很难再变更了,就算你发行新的版本的API,你还要驱动各种调用方升级他们的调用方式。所以,接口设计就像数据库模式设计一下,一旦设计好了,未来再变更就比较难了。所以,还是要好好设计。正如前面我给的几个文档——Microsoft REST API GuidelinesPaypal API Design Guidelines 或是 Google API Design Guide 都是让你好好设计API的不错的 Guidelines.

    3)POST 更安全吗?

    不会。

    很多同学以为 GET 的请求数据在URL中,而 POST 的则不是,所以以为 POST 更安全。不是这样的,整个请求的HTTP URL PATH会全部封装在HTTP的协议头中。只要是HTTPS,就是安全的。当然,有些网关如nginx会把URL打到日志中,或是会放在浏览器的历史记录中,所以有人会说 GET 请求不安全,但是,POST 也没有好到哪里去,在 CSRF 这个最常见的安全问题上,则完全就是针对 POST 的。  安全是一件很复杂的事,无论你用哪方法或动词都会不能代表你会更安全。

    另外,

    • 如果你要 防止你的 GET 上有敏感信息,应该加个密,这个跟 POST是一样的。
    • 如果你要防止 GET 会被中间人修改,你应该做一个URL签名。(通常来说, 我们都在 GET 上做签名,POST 就忘做了)
    • 如果你要防止有人发一些恶意链接来 hack 你的用户(传说中的 GET 不如 POST 安全的一个问题),你应该用 HMAC 之类的认证技术做好认证(参看 HTTP API 认证授权术)。

    总之,你要明白,GETPOST 的安全问题都一样的,不要有谁比谁更安全,然后你就可以掉以轻心的这样的想法,安全都是要很严肃对待的。

    4)全用 POST 可以节省时间减少沟通吗?

    不但不会,反而更糟糕。

    说这种话的人,我感觉是不会思考问题。

    • 其一,为API赋于不同的动词,这个几乎不需要时间。把CRUD写在不同的函数下也是一种很好的编程风格。另外现在几乎所有的开发框架都支持很快速的CRUD的开发,比如Spring Boot,写数据库的CRUD基本上就不需要写SQL语言相关的查询代码,非常之方便。
    • 其二,使用规范的方式,可以节约新加入团队人员的学习成本,而且可以大大减少跨团队的沟能成本。规范和标准其实就是在节约团队时间提升整体效率的,这个我们整个人类进行协作的基础。所以,这个世界上有很多的标准,你只要照着这个标准来,你的所生产的零件就可以适配到其它厂商的产品上。而不需要相互沟通。
    • 其三,全用POST接口一把梭,不规范不标准,使用你的这个山寨API的人就得来不断的问你,反而增加了沟通。另外,也许你开发业务功能很快了,但是你在做控制逻辑的时候,你就要返工了,从长期上来讲,你的欠下了技术债,这个债反而导致了更大的成本。
    5)早点回家的正确姿势

    不要以为你回家早就没事了,如果你的代码有这样那样的问题,别人看懂,或是出误用了你的代码出了问题,那么,你早回家有什么意义呢?你一样要被打扰,甚至被叫到公司来处理问题。所以,你应该做的是为了“长期的早回家”,而不是“短期的早回家”,要像长期的早回家,通常来说是这样的:

    • 把代码组织设计好,有更好的扩展性。这样在面对新需求的时候,你就可以做到少改代码,甚至不改代码。这样你才可能早回家。不然,每次需求一来,你得重新写,你怎么可能早回家?
    • 你的代码质量是不错的,有不错的文档和注释。所以,别人不会老有问题来找你,或是你下班后,叫你来处理问题。甚至任何人都可以很容易地接手你的代码,这样你才可能真正不被打扰
    6)工作而已,优雅不能当饭吃

    回应两点:

    其一,遵循个规范而已,把“正常”叫“优雅”,可见标准有多低。这么低的标准也只能“为了吃饭而生存了”。

    其二,作为一个“职业程序员”,要学会热爱和尊重自己的职业,热爱自己职业最重要的就是不要让外行人看扁这个职业,自己都不尊重这个职业,你让别人怎么尊重?尊重自己的职业,不仅仅只是能够获得让人羡慕的报酬,而更是要让自己的这个职业的更有含金量

    希望大家都能尊重自己从事的这个职业,成为真正的职业化的程序员,而不是一个码农!

    quote your job gives you authority your behavior gives you respect irwin federman 73 55 75
    你的工作给你权力,而只有你的行为才会给你尊重

    (全文完)

    (转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

    The post “一把梭:REST API 全用 POST” first appeared on 酷 壳 – CoolShell.