本文转载自微信公众号《编码迪士尼》,作者陈毅。转载本文请联系CodingDisney公众号。大数据时代对高并发、高可用、了解微服务系统设计的人才需求很大。如果你想从事后台开发,在京东的描述中最常见的要求就是所谓的“高并发”系统开发经验。但是我发现市面上并没有直接针对“高并发”和“高可用”的教程。你找到的信息往往只是寥寥数语,或者是解释那些令人费解的理论。但技术的掌握必须来自于实践。找了很久,发现实践基于微服务的高并发系统开发的教程很少。因此,希望结合自己的学习和实践经验,把这项技术分享给大家。特别强调具体的动手实践,以理解和掌握分布式系统设计的理论和技术。所谓“微服务”,其实也没什么神奇的。它只是将我们原本聚合的模块分解成多个独立的,基于服务端程序的形式,假设我们开发的后台系统分为日志、存储、业务。以往,这些模块会聚合成一个整体,形成一个复杂庞大的应用程序:这种方式存在很多问题,首先是过多的模块组合会使系统设计过于复杂,因为模块直接存在于各种逻辑上的耦合使得系统随着时间的推移越来越难以开发和维护。二是制度越来越脆弱。只要其中一个模块发出错误或崩溃,整个系统就可能崩溃。三是可扩展性不强,系统难以通过硬件性能的提升实现相应的扩展。要实现高并发和高可用,基本思路是将模块拆解,然后让它们成为独立的服务器程序,各个模块之间的协作通过发送消息来完成:这种模式的优点是:1.相互解耦,一个模块故障对整个系统的影响很小。2.可扩展和高可用。我们可以将模块部署到不同的服务器。当流量增加时,我们可以通过简单地增加服务器数量来扩展系统的响应能力。3.增强的鲁棒性。由于可以备份多个模块,如果其中一个模块出现故障,可以将请求重定向到同类型的其他模块,从而大大增强了系统的可靠性。当然,任何好处都有相应的代价,分布式系统的设计和开发会比原来的聚合系统更难。例如负载均衡、服务发现、模块协商、共识达成等。分布式算法强调解决这些问题,但理论总是抽象难懂。如果你不能手工实现一个高可用和高并发的系统,你看到了多少理论?看雾里看花,越看越糊涂,所以一定要通过实践去理解和掌握理论。首先我们从最简单的服务开始,也就是日志服务,我们将使用GO来实现。首先创建一个根目录,可以命名为go_distributed_system。后续所有的服务模块都在这个目录下实现,然后创建一个子目录proglog。进入后,我们创建一个子目录internal/server/。这里我们实现日志服务的逻辑模块,首先在internal/server下执行初始化命令:gomodinitinternal/server这里开发的模块会被其他模块引用,所以我们需要创建mod文件。首先,我们需要完成日志系统所需的底层数据结构,创建一个log.go文件。对应代码如下:recordRecord)(uint64,error){c.mu.Lock()deferc.mu.Unlock()record.Offset=uint64(len(c.records))c.records=append(c.records,record)returnrecord.Offset,nil}func(c*Log)Read(offsetuint64)(Record,error){c.mu.Lock()deferc.mu.Unlock()ifoffset>=uint64(len(c.records)){returnRecord{},ErrOffsetNotFound}returnc.records[offset],nil}typeRecordstruct{Value[]byte`json:"value"`Offsetuint64`json:"offset"`}varErrOffsetNotFound=fmt.Errorf("offsetnotfound")由于我们的日志服务会收到日志http服务器程序形式的读写请求,会同时执行多个读写请求,所以我们需要对records数组进行互斥操作,所以在每个请求之前使用互斥量获取锁读取记录数组,防止服务破坏数据一致性同时接收到多个读写请求时的cy。所有的日志读写请求都会通过httpPOST和GET发起,数据会被json封装,所以我们创建一个http服务器对象,新建一个文件http.go,完成如下代码:packageserverimport("编码/json""net/http""github.com/gorilla/mux")funcNewHttpServer(addrstring)*http.Server{httpsrv:=newHttpServer()r:=mux.NewRouter()r.HandleFunc("/",httpsrv.handleLogWrite)。方法("POST")r.HandleFunc("/",httpsrv.hadnleLogRead).Methods("GET")return&http.Server{Addr:addr,Handler:r,}}typehttpServerstruct{Log*Log}funcnewHttpServer()*httpServer{return&httpServer{Log:NewLog(),}}typeWriteRequeststruct{RecordRecord`json:"record"`}typeWriteResponsestruct{Offsetuint64`json:"offset"`}typeReadRequeststruct{Offsetuint64`json:"offset"`}typeReadResponsestruct{RecordRecord`json:"record"`}func(s*httpServer)handleLogWrite(whttp.ResponseWriter,r*http.Request){varreqWriteRequest//服务收到json格式的请求err:=json.NewDecoder(r.Body).Decode(&req)iferr!=nil{http.Error(w,err.Error(),http.StatusBadRequest)return}off,err:=s.Log.Append(req.Record)iferr!=nil{http.Error(w,err.Error(),http.StatusInternalServerError)return}res:=WriteResponse{Offset:off}//服务以json格式返回结果err=json.NewEncoder(w).Encode(res)iferr!=nil{http.Error(w,err.Error(),http.StatusInternalServerError)return}}func(s*httpServer)hadnleLogRead(whttp.ResponseWriter,r*http.Request){varreqReadRequesterr:=json.NewDecoder(r.Body).解码(&req)iferr!=nil{http.Error(w,err.Error(),http.StatusBadRequest)return}记录,err:=s.Log.Read(req.Offset)iferr==ErrOffsetNotFound{http.Error(w,err.错误(),http.StatusNotFound)return}iferr!=nil{http.Error(w,err.Error(),http.StatusInternalServerError)return}res:=ReadResponse{Record:record}err=json.NewEncoder(w).Encode(res)iferr!=nil{http.Error(w,err.Error(),http.StatusInternalServerError)return}}上面的代码体现了“分布式”和“微服务”的特点相应的功能代码以独立服务器的形式运行,通过网络接收服务请求,对应“分布式”,每个独立的模块只完成特定的任务,对应“微服务”,因为可以采用这种方式同时在不同的机器上运行,从而展示了“可扩展性”。同时,由于服务是以http服务器的形式存在,所以服务的请求和返回也必须是Http的形式,数据以Json的形式进行封装。同时实现的逻辑很简单,就是当有写日志请求时,我们将请求解析成一个Record结构,加入到队列尾部。当有读取日志的请求时,我们获取客户端发送的读取偏移量,然后取出对应的记录,封装成json格式返回给客户端。完成服务器的代码后,我们需要运行服务器。为了达到模块化的目的,我们把服务端的启动放在另外一个地方,在proglog根目录下创建cmd/server,在里面添加main.go:packagemainimport("log""internal/server")funcmain(){srv:=server.NewHttpServer(":8080")log.Fatal(srv.ListenAndServe())}同时,为了能够引用internal/server下的模块,我们需要在cmd下/server,首先通过gomodinitcmd/server初始化,然后在go.mod文件中添加如下一行:replaceinternal/server=>../../internal/server然后执行命令gomodtidy,这样本地的模块就知道根据给定的目录进行转换和引用模块,最后使用gorunmain.go启动日志服务。现在我们需要做的是测试服务器的可用性。我们也在该目录下创建server_test.go,然后编写测试代码。基本逻辑是向服务端发送日志写入请求,然后发送读取请求,比较读取的数据和我们写入的数据是否一致。代码如下:packagemainimport("encoding/json""net/http""internal/server""bytes""testing""io/ioutil")funcTestServerLogWrite(t*testing.T){vartests=[]struct{requestserver.WriteRequestwant_responseserver.WriteResponse}{{request:server.WriteRequest{server.Record{[]byte(`thisislogrequest1`),0}},want_response:server.WriteResponse{Offset:0,},},{request:server.WriteRequest{server.Record{[]byte(`thisislogrequest2`),0}},want_response:server.WriteResponse{Offset:1,},},{request:server.WriteRequest{server.Record{[]byte(`thisislogrequest3`),0}},want_response:server.WriteResponse{Offset:2,},},}for_,test:=rangetests{//将请求转成json格式投递到日志服务request:=&test.requestrequest_json,err:=json.Marshal(request)iferr!=nil{t.Errorf("convertrequesttojsonfail")return}resp,err:=http.Post("http://localhost:8080","application/json",bytes.NewBuffer(request_json))deferresp.Body.Close()iferr!=nil{t.Errorf("httppostrequestfail:%v",err)return}//返回结果body,err:=ioutil.ReadAll(resp.Body)varresponseserver.WriteResponseerr=json.Unmarshal([]byte(body),&response)iferr!=nil{t.Errorf("Unmarshalwriteresponsefail:%v",err)}//检测结果是否与预期一致ifresponse.Offset!=test.want_response.Offset{t.Errorf("gotoffset:%d,butwantoffset:%d",response.Offset,test.want_response.Offset)}}varread_tests=[]struct{requestserver.ReadRequestwantserver.ReadResponse}{{request:server.ReadRequest{Offset:0,},want:server.ReadResponse{server.Record{[]byte(`thisislogrequest1`),0}}},{request:server.ReadRequest{Offset:1,},want:server.ReadResponse{server.Record{[]byte(`thisislogrequest2`),0}}},{request:server.ReadRequest{Offset:2,},想要:server.ReadResponse{server.Record{[]byte(`thisislogrequest3`),0}}},}for_,test:=rangeread_tests{request:=test.requestrequest_json,err:=json.Marshal(request)iferr!=nil{t.Errorf("convertreadrequesttojsonfail")return}//将请求转换为json并放入GET请求体client:=&http.Client{}req,err:=http.NewRequest(http.MethodGet,"http://localhost:8080",bytes.NewBuffer(request_json))req.Header.Set("Content-Type","application/json")resp,err:=client.Do(req)iferr!=nil{t.Errorf("readrequestfail:%v",err)return}//解析读取请求返回的结果deferresp.Body.Close()body,err:=ioutil.ReadAll(resp.Body)varresponseserver.ReadResponseerr=json.Unmarshal([]byte(body),&response)iferr!=nil{t.Errorf("Unmarshalreadresponsefail:%v",err)return}res:=bytes.Compare(response.Record.Value,test.want.Record.Value)ifres!=0{t.Errorf("gotvalue:%q,butwantvalue:%q",response.Record.Value,test.want.Record.Value)}}}完成以上代码后,使用go测试运行,结果如下图所示:从结果可以看出我们的测试是可以通过的,也就是无论我们向日志服务提交写请求还是读请求,得到的结果符合我们的预期。综上所述,本节我们设计了一个简单的JSON/HTTP日志服务,可以接收基于JSON的http写请求和读请求。后面会研究基于gPRC技术的微服务开发技术。代码获取https://github.com/wycl16514/golang_distribute_system_log_service.git
