范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

GOWEB常见大型Web项目分层,灰度发布和ABtest

  流行的Web框架大多数是MVC框架,MVC这个概念最早由Trygve Reenskaug在1978年提出,为了能够对GUI类型的应用进行方便扩展,将程序划分为:  控制器(Controller)- 负责转发请求,对请求进行处理。  视图(View) - 界面设计人员进行图形界面设计。  模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
  随着时代的发展,前端也变成了越来越复杂的工程,为了更好地工程化,现在更为流行的一般是前后分离的架构。可以认为前后分离是把V层从MVC中抽离单独成为项目。这样一个后端项目一般就只剩下 M和C层了。前后端之间通过ajax来交互,有时候要解决跨域的问题,但也已经有了较为成熟的方案。
  前后分离交互图
  图里的Vue和React是现在前端界比较流行的两个框架,因为我们的重点不在这里,所以前端项目内的组织我们就不强调了。事实上,即使是简单的项目,业界也并没有完全遵守MVC框架提出者对于M和C所定义的分工。有很多公司的项目会在Controller层塞入大量的逻辑,在Model层就只管理数据的存储。这往往来源于对于model层字面含义的某种擅自引申理解。认为字面意思,这一层就是处理某种建模,而模型是什么?就是数据呗!
  这种理解显然是有问题的,业务流程也算是一种"模型",是对真实世界用户行为或者既有流程的一种建模,并非只有按格式组织的数据才能叫模型。不过按照MVC的创始人的想法,我们如果把和数据打交道的代码还有业务流程全部塞进MVC里的M层的话,这个M层又会显得有些过于臃肿。对于复杂的项目,一个C和一个M层显然是不够用的,现在比较流行的纯后端API模块一般采用下述划分方法:  Controller,与上述类似,服务入口,负责处理路由,参数校验,请求转发。  Logic/Service,逻辑(服务)层,一般是业务逻辑的入口,可以认为从这里开始,所有的请求参数一定是合法的。业务逻辑和业务流程也都在这一层中。常见的设计中会将该层称为 Business Rules。  DAO/Repository,这一层主要负责和数据、存储打交道。将下层存储以更简单的函数、接口形式暴露给 Logic 层来使用。负责数据的持久化工作。
  每一层都会做好自己的工作,然后用请求当前的上下文构造下一层工作所需要的结构体或其它类型参数,然后调用下一层的函数。在工作完成之后,再把处理结果一层层地传出到入口。
  请求处理流程
  划分为CLD三层之后,在C层之前我们可能还需要同时支持多种协议。本章前面讲到的thrift、gRPC和http并不是一定只选择其中一种,有时我们需要支持其中的两种,比如同一个接口,我们既需要效率较高的thrift,也需要方便debug的http入口。即除了CLD之外,还需要一个单独的protocol层,负责处理各种交互协议的细节。这样请求的流程就会变成下图所示。
  这样我们Controller中的入口函数就变成了下面这样:  func CreateOrder(ctx context.Context, req *CreateOrderStruct) (     *CreateOrderRespStruct, error, ) {     // ... }
  CreateOrder有两个参数,ctx用来传入trace_id一类的需要串联请求的全局参数,req里存储了我们创建订单所需要的所有输入信息。返回结果是一个相应结构的错误。可以认为,我们的代码运行到Controller层之后,就没有任何与"协议"相关的代码了。在这里你找不到 http.Request  ,也找不到 http.ResponseWriter  ,也找不到任何与thrift或者gRPC相关的字眼。
  在协议(Protocol)层,处理http协议的大概代码如下:  // defined in protocol layer type CreateOrderRequest struct {     OrderID int64 `json:"order_id"`     // ... }  // defined in controller type CreateOrderParams struct {     OrderID int64 }  func HTTPCreateOrderHandler(wr http.ResponseWriter, r *http.Request) {     var req CreateOrderRequest     var params CreateOrderParams     ctx := context.TODO()     // bind data to req     bind(r, &req)     // map protocol binded to protocol-independent     map(req, params)     logicResp,err := controller.CreateOrder(ctx, ¶ms)     if err != nil {}     // ... }
  理论上我们可以用同一个请求结构体组合上不同的tag,来达到一个结构体来给不同的协议复用的目的。不过遗憾的是在thrift中,请求结构体也是通过IDL生成的,其内容在自动生成的ttypes.go文件中,我们还是需要在thrift的入口将这个自动生成的结构体映射到我们logic入口所需要的结构体上。gRPC也是类似。这部分代码还是需要的。
  聪明的读者可能已经可以看出来了,协议细节处理这一层有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的结构体(例如 http.Request  ,thrift的被包装过了) 读出来,再绑定到我们协议无关的结构体上,再把这个结构体映射到Controller入口的结构体上,这些代码长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用代码生成的方式,把繁复的协议处理代码从工作内容中抽离出去。
  先来看看HTTP对应的结构体、thrift对应的结构体和我们协议无关的结构体分别长什么样子:  // http 请求结构体 type CreateOrder struct {     OrderID   int64  `json:"order_id" validate:"required"`     UserID    int64  `json:"user_id" validate:"required"`     ProductID int    `json:"prod_id" validate:"required"`     Addr      string `json:"addr" validate:"required"` }  // thrift 请求结构体 type FeatureSetParams struct {     DriverID  int64  `thrift:"driverID,1,required"`     OrderID   int64  `thrift:"OrderID,2,required"`     UserID    int64  `thrift:"UserID,3,required"`     ProductID int    `thrift:"ProductID,4,required"`     Addr      string `thrift:"Addr,5,required"` }  // controller input struct type CreateOrderParams struct {     OrderID int64     UserID int64     ProductID int     Addr string }
  我们需要通过一个源结构体来生成我们需要的HTTP和thrift入口代码。再观察一下上面定义的三种结构体,我们只要能用一个结构体生成thrift的IDL,以及HTTP服务的"IDL(只要能包含json或form相关tag的结构体定义信息)" 就可以了。这个初始的结构体我们可以把结构体上的HTTP的tag和thrift的tag揉在一起:  type FeatureSetParams struct {     DriverID  int64  `thrift:"driverID,1,required" json:"driver_id"`     OrderID   int64  `thrift:"OrderID,2,required" json:"order_id"`     UserID    int64  `thrift:"UserID,3,required" json:"user_id"`     ProductID int    `thrift:"ProductID,4,required" json:"prod_id"`     Addr      string `thrift:"Addr,5,required" json:"addr"` }
  然后通过代码生成把thrift的IDL和HTTP的请求结构体都生成出来,如  图 5-16所示
  至于用什么手段来生成,你可以通过Go语言内置的Parser读取文本文件中的Go源代码,然后根据AST来生成目标代码,也可以简单地把这个源结构体和Generator的代码放在一起编译,让结构体作为Generator的输入参数(这样会更简单一些),都是可以的。
  当然这种思路并不是唯一选择,我们还可以通过解析thrift的IDL,生成一套HTTP接口的结构体。如果你选择这么做,那整个流程就变成了  图 5-17  所示。
  看起来比之前的图顺畅一点,不过如果你选择了这么做,你需要自行对thrift的IDL进行解析,也就是相当于可能要手写一个thrift的IDL的Parser,虽然现在有Antlr或者peg能帮你简化这些Parser的书写工作,但在"解析"的这一步我们不希望引入太多的工作量,所以量力而行即可。
  既然工作流程已经成型,我们可以琢磨一下怎么让整个流程对用户更加友好。
  比如在前面的生成环境引入Web页面,只要让用户点点鼠标就能生成SDK,这些就靠读者自己去探索了。
  虽然我们成功地使自己的项目在入口支持了多种交互协议,但是还有一些问题没有解决。本节中所叙述的分层没有将中间件作为项目的分层考虑进去。如果我们考虑中间件的话,请求的流程是什么样的?
  之前我们学习的中间件是和HTTP协议强相关的,遗憾的是在thrift中看起来没有和HTTP中对等的解决这些非功能性逻辑代码重复问题的中间件。所以我们在图上写 thrift stuff  。这些 stuff  可能需要你手写去实现,然后每次增加一个新的thrift接口,就需要去写一遍这些非功能性代码。  灰度发布和 A/B test
  中型的互联网公司往往有着以百万计的用户,而大型互联网公司的系统则可能要服务千万级甚至亿级的用户需求。大型系统的请求流入往往是源源不断的,任何风吹草动,都一定会有最终用户感受得到。例如你的系统在上线途中会拒绝一些上游过来的请求,而这时候依赖你的系统没有做任何容错,那么这个错误就会一直向上抛出,直到触达最终用户。形成一次对用户切切实实的伤害。这种伤害可能是在用户的APP上弹出一个让用户摸不着头脑的诡异字符串,用户只要刷新一下页面就可以忘记这件事。但也可能会让正在心急如焚地和几万竞争对手同时抢夺秒杀商品的用户,因为代码上的小问题,丧失掉了先发优势,与自己蹲了几个月的心仪产品失之交臂。对用户的伤害有多大,取决于你的系统对于你的用户来说有多重要。
  不管怎么说,在大型系统中容错是重要的,能够让系统按百分比,分批次到达最终用户,也是很重要的。虽然当今的互联网公司系统,名义上会说自己上线前都经过了充分慎重严格的测试,但就算它们真的做到了,代码的bug总是在所难免的。即使代码没有bug,分布式服务之间的协作也是可能出现"逻辑"上的技术问题的。
  这时候,灰度发布就显得非常重要了,灰度发布也称为金丝雀发布,传说17世纪的英国矿井工人发现金丝雀对瓦斯气体非常敏感,瓦斯达到一定浓度时,金丝雀即会死亡,但金丝雀的致死量瓦斯对人并不致死,因此金丝雀被用来当成他们的瓦斯检测工具。互联网系统的灰度发布一般通过两种方式实现:  通过分批次部署实现灰度发布  通过业务规则进行灰度发布
  在对系统的旧功能进行升级迭代时,第一种方式用得比较多。新功能上线时,第二种方式用得比较多。当然,对比较重要的老功能进行较大幅度的修改时,一般也会选择按业务规则来进行发布,因为直接全量开放给所有用户的风险实在太大。  通过分批次部署实现灰度发布
  假如服务部署在15个实例(可能是物理机,也可能是容器)上,我们把这15个实例分为四组,按照先后顺序,分别有1-2-4-8台机器,保证每次扩展时大概都是二倍的关系。
  为什么要用2倍?这样能够保证我们不管有多少台机器,都不会把组划分得太多。例如1024台机器,也就只需要1-2-4-8-16-32-64-128-256-512部署十次就可以全部部署完毕。
  这样我们上线最开始影响到的用户在整体用户中占的比例也不大,比如1000台机器的服务,我们上线后如果出现问题,也只影响1/1000的用户。如果10组完全平均分,那一上线立刻就会影响1/10的用户,1/10的业务出问题,那可能对于公司来说就已经是一场不可挽回的事故了。
  在上线时,最有效的观察手法是查看程序的错误日志,如果较明显的逻辑错误,一般错误日志的滚动速度都会有肉眼可见的增加。这些错误也可以通过metrics一类的系统上报给公司内的监控系统,所以在上线过程中,也可以通过观察监控曲线,来判断是否有异常发生。
  如果有异常情况,首先要做的自然就是回滚了。  通过业务规则进行灰度发布
  常见的灰度策略有多种,较为简单的需求,例如我们的策略是要按照千分比来发布,那么我们可以用用户id、手机号、用户设备信息,等等,来生成一个简单的哈希值,然后再求模,用伪代码表示一下:  // pass 3/1000 func passed() bool {     key := hashFunctions(userID) % 1000     if key <= 2 {         return true     }      return false } 可选规则
  常见的灰度发布系统会有下列规则提供选择:  按城市发布  按概率发布  按百分比发布  按白名单发布  按业务线发布  按UA发布(APP、Web、PC)  按分发渠道发布
  因为和公司的业务相关,所以城市、业务线、UA、分发渠道这些都可能会被直接编码在系统里,不过功能其实大同小异。
  按白名单发布比较简单,功能上线时,可能我们希望只有公司内部的员工和测试人员可以访问到新功能,会直接把账号、邮箱写入到白名单,拒绝其它任何账号的访问。
  按概率发布则是指实现一个简单的函数:  func isTrue() bool {     return true/false according to the rate provided by user }
  其可以按照用户指定的概率返回 true  或者 false  ,当然, true  的概率加 false  的概率应该是100%。这个函数不需要任何输入。
  按百分比发布,是指实现下面这样的函数:  func isTrue(phone string) bool {     if hash of phone matches {         return true     }      return false }
  这种情况可以按照指定的百分比,返回对应的 true  和 false  ,和上面的单纯按照概率的区别是这里我们需要调用方提供给我们一个输入参数,我们以该输入参数作为源来计算哈希,并以哈希后的结果来求模,并返回结果。这样可以保证同一个用户的返回结果多次调用是一致的,在下面这种场景下,必须使用这种结果可预期的灰度算法。
  先set然后马上get
  如果采用随机策略,可能会出现像这样的问题:
  举个具体的例子,网站的注册环节,可能有两套API,按照用户ID进行灰度,分别是不同的存取逻辑。如果存储时使用了V1版本的API而获取时使用V2版本的API,那么就可能出现用户注册成功后反而返回注册失败消息的诡异问题。  如何实现一套灰度发布系统
  前面也提到了,提供给用户的接口大概可以分为和业务绑定的简单灰度判断逻辑。以及输入稍微复杂一些的哈希灰度。我们来分别看看怎么实现这样的灰度系统(函数)。  业务相关的简单灰度
  公司内一般都会有公共的城市名字和id的映射关系,如果业务只涉及中国国内,那么城市数量不会特别多,且id可能都在10000范围以内。那么我们只要开辟一个一万大小左右的bool数组,就可以满足需求了:  var cityID2Open = [12000]bool{}  func init() {     readConfig()     for i:=0;i= 100 {         return true     }      if rate > 0 && rand.Int(100) > rate {         return true     }      return false }
  注意初始化种子。  哈希算法
  求哈希可用的算法非常多,比如md5,crc32,sha1等等,但我们这里的目的只是为了给这些数据做个映射,并不想要因为计算哈希消耗过多的cpu,所以现在业界使用较多的算法是murmurhash,下面是我们对这些常见的hash算法的简单benchmark。
  下面使用了标准库的md5,sha1和开源的murmur3实现来进行对比。  package main  import (     "crypto/md5"     "crypto/sha1"      "github.com/spaolacci/murmur3" )  var str = "hello world"  func md5Hash() [16]byte {     return md5.Sum([]byte(str)) }  func sha1Hash() [20]byte {     return sha1.Sum([]byte(str)) }  func murmur32() uint32 {     return murmur3.Sum32([]byte(str)) }  func murmur64() uint64 {     return murmur3.Sum64([]byte(str)) }
  为这些算法写一个基准测试:  package main  import "testing"  func BenchmarkMD5(b *testing.B) {     for i := 0; i < b.N; i++ {         md5Hash()     } }  func BenchmarkSHA1(b *testing.B) {     for i := 0; i < b.N; i++ {         sha1Hash()     } }  func BenchmarkMurmurHash32(b *testing.B) {     for i := 0; i < b.N; i++ {         murmur32()     } }  func BenchmarkMurmurHash64(b *testing.B) {     for i := 0; i < b.N; i++ {         murmur64()     } }
  然后看看运行效果:  ~/t/g/hash_bench git:master ❯❯❯ go test -bench=. goos: darwin goarch: amd64 BenchmarkMD5-4          10000000 180 ns/op BenchmarkSHA1-4         10000000 211 ns/op BenchmarkMurmurHash32-4 50000000  25.7 ns/op BenchmarkMurmurHash64-4 20000000  66.2 ns/op PASS ok _/Users/caochunhui/test/go/hash_bench 7.050s
  可见murmurhash相比其它的算法有三倍以上的性能提升。显然做负载均衡的话,用murmurhash要比md5和sha1都要好,这些年社区里还有另外一些更高效的哈希算法涌现,感兴趣的读者可以自行调研。  分布是否均匀
  对于哈希算法来说,除了性能方面的问题,还要考虑哈希后的值是否分布均匀。如果哈希后的值分布不均匀,那也自然就起不到均匀灰度的效果了。
  以murmur3为例,我们先以15810000000开头,造一千万个和手机号类似的数字,然后将计算后的哈希值分十个桶,并观察计数是否均匀:  package main  import (     "fmt"      "github.com/spaolacci/murmur3" )  var bucketSize = 10  func main() {     var bucketMap = map[uint64]int{}     for i := 15000000000; i < 15000000000+10000000; i++ {         hashInt := murmur64(fmt.Sprint(i)) % uint64(bucketSize)         bucketMap[hashInt]++     }     fmt.Println(bucketMap) }  func murmur64(p string) uint64 {     return murmur3.Sum64([]byte(p)) }
  看看执行结果:  map[7:999475 5:1000359 1:999945 6:1000200 3:1000193 9:1000765 2:1000044  4:1000343 8:1000823 0:997853]
  偏差都在1/100以内,可以接受。在调研其它算法,并判断是否可以用来做灰度发布时,也应该从本节中提到的性能和均衡度两方面出发,对其进行考察。

胎心监护就是听一听宝宝心跳吗?到了孕晚期,孕妈妈会发现自己的产检项目中,增加了一项胎心监护,而且医生要求每周都要做。不少孕妈妈认为胎心监护不就是坐在那里听听宝宝心跳吗?我家里买了胎心仪,有必要频繁往医院跑吗?很吃腻了土豆泥,来试试这样做的土豆饼,咬一口糯叽叽还能拉丝绵软香甜的土豆,也是我们辅食中的常客了,易咀嚼易消化,非常适合宝宝吃。土豆的做法也非常多样,如果吃腻了土豆泥,不如来试试这样做的土豆饼,咬一口糯叽叽还能拉丝,哪个宝宝不爱这么吃?适当妈的崩溃,都是被这些小事治愈的!第一条差点哭出来当妈后,娃做过哪些一瞬间让你被治愈的事?又有哪些瞬间在你的记忆里闪闪发光?前段时间糕妈发了一条微博去年这个时候发发还闹分离焦虑,像个牛皮糖一样,每次一看到我们要走,就哭得那叫一个惊RNG躺进S12条件苛刻!一切在于EDG!每一局都是关键英雄联盟2022LPL夏季季后赛正如火如荼地进行中,对于之前常规赛最后时刻那戏剧性的对决,一环套着一环,每一个小名次都会影响到之后的大局!如今到了季后赛也是如此,尤其是关乎着S12为什么说现在依旧是入坑激战2国服的最佳时期?作者小黑盒野哉诸人一基础介绍激战2贯彻着设计师叛逆而大胆,激进而浪漫的意志。三位来自魔兽争霸3魔兽世界和战网项目的前暴雪员工将他们独特的理念投入到激战1,激战2一脉相承。与激战1一胎心监护就是听一听宝宝心跳吗?到了孕晚期,孕妈妈会发现自己的产检项目中,增加了一项胎心监护,而且医生要求每周都要做。不少孕妈妈认为胎心监护不就是坐在那里听听宝宝心跳吗?我家里买了胎心仪,有必要频繁往医院跑吗?很吃腻了土豆泥,来试试这样做的土豆饼,咬一口糯叽叽还能拉丝绵软香甜的土豆,也是我们辅食中的常客了,易咀嚼易消化,非常适合宝宝吃。土豆的做法也非常多样,如果吃腻了土豆泥,不如来试试这样做的土豆饼,咬一口糯叽叽还能拉丝,哪个宝宝不爱这么吃?适当妈的崩溃,都是被这些小事治愈的!第一条差点哭出来当妈后,娃做过哪些一瞬间让你被治愈的事?又有哪些瞬间在你的记忆里闪闪发光?前段时间糕妈发了一条微博去年这个时候发发还闹分离焦虑,像个牛皮糖一样,每次一看到我们要走,就哭得那叫一个惊大片播放器,打造私人影院随着社会的发展,生活水平的不断提高,我们对生活的品质也有了更高的要求,不管是家庭娱乐,还是办公,都需要一款好的播放器,能够让你的生活更加丰富多彩。接下来为大家推荐几款播放机,小巧的大片播放器,打造私人影院随着社会的发展,生活水平的不断提高,我们对生活的品质也有了更高的要求,不管是家庭娱乐,还是办公,都需要一款好的播放器,能够让你的生活更加丰富多彩。接下来为大家推荐几款播放机,小巧的面临退役!CBA名将因伤无球可打,多次想加盟辽篮未成功没有找到工作的球员要抓紧时间了虽然8月31日快到了,但还有很多球员都没有找到工作,比如被山东队买断的王汝恒,离开上海队的宗赞,离开广厦的冯欣,等等,留给他们的时间不多了,极有可能要
哑巴吃黄连,有苦说不出,什么时候可以用黄连?说说中医妙用黄连中国有句古话哑巴吃黄连,有苦说不出,提起黄连,我们都有一个印象,那就是肯定味道极苦,确实,它在中药苦味排行榜中,名列前排,而且咀嚼黄连时,唾液可被染成红黄色,故名黄连。不过,良药苦好吃的食物,男女记得吃,预防疾病,促进排毒,养生保健微量元素生活在这个色彩斑斓的世界上,我们享受着大自然赋予我们的各种美好。但是,为了生计,我们也承受着较大的压力,久而久之,这让很多人的身体处于一种亚健康的状态。我呼吁大家要注意自己的身体健春季瘙痒皮肤,起小红点,会不由自主地想去挠,导致皮肤肿胀春季手上脸部脚部等皮肤瘙痒,起小红点,又红又痒,会不由自主地想去挠,挠完舒服一阵子又去挠,导致皮肤肿胀瘙痒难耐,甚至会破皮。这些都是什么原因导致的呢?很多人说这是皮疹花粉过敏皮炎等春天不养肝,夏天徒伤悲在中医看来春主生发,春应肝,春季是肝疏泄功能最强的时候。从现代医学的角度来看,在春季,肝脏要承担更多的机体调节工作,让我们的身体保持健康和活力。在此时配合节气特点,合理养肝,才能起这6种食物没有保质期,即使存放时间很长也可以吃,不要浪费人体的各项组织器官和组织细胞的正常运转是通过食物当中的营养素来获取的一种有氧含量和机体的有氧运转,想要维护器官的正常运行,首先保证食物的质量和营养素的补充状态。了解体内个性组织器官后宫001吴宣仪亮眼的瞬间,两只手不要松开手机哟可以说她没有演技,但不能不承认他的身材很迷人可以说她唱歌跑调,但不能否定她的龅牙小嘴别有一番韵味可以说她跳舞没节奏,但她的一双长腿比舞蹈更值得欣赏。无的放矢的长腿这是吴宣仪的成名照你能坚持一年不买衣服吗?女人的衣柜永远都会差一件衣服,要说起衣服,走进商场,可谓是五花八门,只要你想买,总会有适合你的。回到家里,发现衣柜里衣服很多,却发现总是差一件,不知道明天穿什么。我们在询问60岁左想不到牛仔裤春外套火了,气质高级又很清爽,日常穿刚刚好牛仔裤在裤装种类中可以说是一枝独秀的存在,百搭又减龄,不管是十几二十岁的小女生还是三四十岁的熟龄女性都能轻松驾驭。可越是基础百搭的单品越考验穿搭功底,牛仔裤也不例外,要想降低撞衫率有一种高级感,叫做美得自然范主说天然去雕饰最近,安吉丽娜朱莉在INS上晒出带女儿访问柬埔寨的照片,照片上的她,身着一身亚麻质感的衣服,轻盈飘逸又裁剪得当,白色和深棕色的搭配,整个氛围流露出一种自然的舒适感。大家用过自制的美白面膜吗?天然又健康大家都知道肌肤要美白,自然少不了面膜,选择天然的护肤材料,不受环境和经济条件的限制,既便宜又好用的美白面膜有好多,它们一样可以让你白得自然!如果还想了解更多护肤化妆知识,修身养颜知苹果手机在俄罗斯被限制部分功能及销售,我们应该反思什么?据报道,在日前,苹果宣布暂停在俄罗斯销售产品,关闭部分功能,并限制苹果支付以及地图位置等相关服务。不仅如此,就连迪士尼也禁止他们旗下的影片在俄罗斯放映。这是什么骚操作?此时此刻,相