Autor | Liu Xiaomin Yu Yu
1. Introducción
En el mundo Java, netty es un marco de comunicación de red de alto rendimiento ampliamente utilizado por todos. Muchos marcos de RPC se implementan basándose en netty. En el mundo de golang, getty es también una biblioteca de comunicación en red de alto rendimiento similar a netty. getty fue desarrollado originalmente por Yu Yu, la persona a cargo del proyecto dubbogo, y utilizado en dubbo-go como biblioteca de comunicación subyacente . Con la donación de dubbo-go a la Fundación Apache, con el esfuerzo conjunto de los socios de la comunidad, getty finalmente ingresó a la familia apache y pasó a llamarse dubbo-getty .
En 18 años, cuando practicaba microservicios en la empresa, el mayor problema que encontré fue el problema de las transacciones distribuidas. En el mismo año, Ali abrió sus soluciones de transacciones distribuidas en la comunidad y rápidamente seguí este proyecto. Al principio, se llamaba fescar, y luego cambió su nombre a seata. Como estoy muy interesado en la tecnología de código abierto, agregué muchos grupos comunitarios, en ese momento también presté mucha atención al proyecto dubbo-go y me sumergí silenciosamente en él. Con la comprensión de seata, surgió gradualmente la idea de hacer una versión go de un marco de transacciones distribuidas.
Para hacer una versión golang de un marco de transacciones distribuidas, la primera pregunta es cómo implementar la comunicación RPC. Dubbo-go es un muy buen ejemplo, así que comencé a estudiar el getty subyacente de dubbo-go.
2. Cómo implementar la comunicación RPC basada en getty
El diagrama de modelo general del marco getty es el siguiente:
A continuación se describe el proceso de comunicación RPC de seata-golang junto con los códigos relacionados.
1. Establecer una conexión
Para realizar la comunicación RPC, primero debemos establecer una conexión de red. Comencemos con client.go .
func (c *client) connect() {
var (
err error
ss Session
)
for {
// 建立一个 session 连接
ss = c.dial()
if ss == nil {
// client has been closed
break
}
err = c.newSession(ss)
if err == nil {
// 收发报文
ss.(*session).run()
// 此处省略部分代码
break
}
// don't distinguish between tcp connection and websocket connection. Because
// gorilla/websocket/conn.go:(Conn)Close also invoke net.Conn.Close()
ss.Conn().Close()
}
}
connect()
Métodos dial()
Método obtener una conexión de sesión en el Método Dial ():
func (c *client) dial() Session {
switch c.endPointType {
case TCP_CLIENT:
return c.dialTCP()
case UDP_CLIENT:
return c.dialUDP()
case WS_CLIENT:
return c.dialWS()
case WSS_CLIENT:
return c.dialWSS()
}
return nil
}
Nos preocupa que haya una conexión TCP, así que continúe con el c.dialTCP()
método:
func (c *client) dialTCP() Session {
var (
err error
conn net.Conn
)
for {
if c.IsClosed() {
return nil
}
if c.sslEnabled {
if sslConfig, err := c.tlsConfigBuilder.BuildTlsConfig(); err == nil && sslConfig != nil {
d := &net.Dialer{Timeout: connectTimeout}
// 建立加密连接
conn, err = tls.DialWithDialer(d, "tcp", c.addr, sslConfig)
}
} else {
// 建立 tcp 连接
conn, err = net.DialTimeout("tcp", c.addr, connectTimeout)
}
if err == nil && gxnet.IsSameAddr(conn.RemoteAddr(), conn.LocalAddr()) {
conn.Close()
err = errSelfConnect
}
if err == nil {
// 返回一个 TCPSession
return newTCPSession(conn, c)
}
log.Infof("net.DialTimeout(addr:%s, timeout:%v) = error:%+v", c.addr, connectTimeout, perrors.WithStack(err))
<-wheel.After(connectInterval)
}
}
Hasta ahora, sabemos cómo getty establece una conexión TCP y regresa a TCPSession.
2. Envía y recibe mensajes
Entonces, ¿cómo envía y recibe mensajes? Volvamos al método de conexión y miremos hacia abajo. Hay una línea ss.(*session).run()
después de esta línea de código. El código es una operación muy simple. Suponemos que la lógica de esta línea de código debe incluir el envío y la recepción de mensajes. lógica de texto, y luego ingresa el run()
método:
func (s *session) run() {
// 省略部分代码
go s.handleLoop()
go s.handlePackage()
}
<br /> Jugué aquí dos goroutine, handleLoop
y handlePackage
veo literalmente a nuestro sospechoso en el handleLoop()
método: <br />
func (s *session) handleLoop() {
// 省略部分代码
for {
// A select blocks until one of its cases is ready to run.
// It choose one at random if multiple are ready. Otherwise it choose default branch if none is ready.
select {
// 省略部分代码
case outPkg, ok = <-s.wQ:
// 省略部分代码
iovec = iovec[:0]
for idx := 0; idx < maxIovecNum; idx++ {
// 通过 s.writer 将 interface{} 类型的 outPkg 编码成二进制的比特
pkgBytes, err = s.writer.Write(s, outPkg)
// 省略部分代码
iovec = append(iovec, pkgBytes)
//省略部分代码
}
// 将这些二进制比特发送出去
err = s.WriteBytesArray(iovec[:]...)
if err != nil {
log.Errorf("%s, [session.handleLoop]s.WriteBytesArray(iovec len:%d) = error:%+v",
s.sessionToken(), len(iovec), perrors.WithStack(err))
s.stop()
// break LOOP
flag = false
}
case <-wheel.After(s.period):
if flag {
if wsFlag {
err := wsConn.writePing()
if err != nil {
log.Warnf("wsConn.writePing() = error:%+v", perrors.WithStack(err))
}
}
// 定时执行的逻辑,心跳等
s.listener.OnCron(s)
}
}
}
}
Por el código anterior, encontramos que handleLoop()
el método es enviar la lógica de procesamiento de mensajes, el mensaje RPC debe enviarse mediante la primera s.writer
codificación en bits binarios y transmitirse a través de la conexión TCP establecida. Esto s.writer
corresponde a Writer RPC Interface es un marco de interfaz que debe lograrse.
Continúe mirando los handlePackage()
métodos:
func (s *session) handlePackage() {
// 省略部分代码
if _, ok := s.Connection.(*gettyTCPConn); ok {
if s.reader == nil {
errStr := fmt.Sprintf("session{name:%s, conn:%#v, reader:%#v}", s.name, s.Connection, s.reader)
log.Error(errStr)
panic(errStr)
}
err = s.handleTCPPackage()
} else if _, ok := s.Connection.(*gettyWSConn); ok {
err = s.handleWSPackage()
} else if _, ok := s.Connection.(*gettyUDPConn); ok {
err = s.handleUDPPackage()
} else {
panic(fmt.Sprintf("unknown type session{%#v}", s))
}
}
En el handleTCPPackage()
método:
func (s *session) handleTCPPackage() error {
// 省略部分代码
conn = s.Connection.(*gettyTCPConn)
for {
// 省略部分代码
bufLen = 0
for {
// for clause for the network timeout condition check
// s.conn.SetReadTimeout(time.Now().Add(s.rTimeout))
// 从 TCP 连接中收到报文
bufLen, err = conn.recv(buf)
// 省略部分代码
break
}
// 省略部分代码
// 将收到的报文二进制比特写入 pkgBuf
pktBuf.Write(buf[:bufLen])
for {
if pktBuf.Len() <= 0 {
break
}
// 通过 s.reader 将收到的报文解码成 RPC 消息
pkg, pkgLen, err = s.reader.Read(s, pktBuf.Bytes())
// 省略部分代码
s.UpdateActive()
// 将收到的消息放入 TaskQueue 供 RPC 消费端消费
s.addTask(pkg)
pktBuf.Next(pkgLen)
// continue to handle case 5
}
if exit {
break
}
}
return perrors.WithStack(err)
}
A partir de la lógica del código anterior, analizamos que el consumidor de RPC necesita decodificar el mensaje de bit binario recibido de la conexión TCP en un mensaje que RPC puede consumir. Este trabajo lo implementa el lector, por lo que necesitamos construir la capa de comunicación RPC. Implementar la interfaz del lector correspondiente al lector.
3. Cómo desacoplar la lógica de procesamiento de mensajes de red en la parte inferior de la lógica empresarial
Todos sabemos que netty logra el desacoplamiento de la lógica de red subyacente y la lógica empresarial a través del hilo del jefe y el hilo del trabajador. Entonces, ¿cómo se implementa getty?
En el handlePackage()
último método, vemos que el mensaje recibido se coloca en s.addTask(pkg)
este método, luego se analiza:
func (s *session) addTask(pkg interface{}) {
f := func() {
s.listener.OnMessage(s, pkg)
s.incReadPkgNum()
}
if taskPool := s.EndPoint().GetTaskPool(); taskPool != nil {
taskPool.AddTaskAlways(f)
return
}
f()
}
pkg
Los parámetros se pasan a un método anónimo, que finalmente se introduce taskPool
. Este método es muy importante.Cuando escribí el código seata-golang más tarde, encontré un pozo, que analizaré más adelante.
Luego miramos la definición de taskPool :
// NewTaskPoolSimple build a simple task pool
func NewTaskPoolSimple(size int) GenericTaskPool {
if size < 1 {
size = runtime.NumCPU() * 100
}
return &taskPoolSimple{
work: make(chan task),
sem: make(chan struct{}, size),
done: make(chan struct{}),
}
}
runtime.NumCPU() * 100
Se construye un canal con un tamaño de búfer de tamaño (predeterminado ) sem
. Mira el método AddTaskAlways(t task)
:
func (p *taskPoolSimple) AddTaskAlways(t task) {
select {
case <-p.done:
return
default:
}
select {
case p.work <- t:
return
default:
}
select {
case p.work <- t:
case p.sem <- struct{}{}:
p.wg.Add(1)
go p.worker(t)
default:
goSafely(t)
}
}
La tarea agregada será consumida por len (p.sem) goroutines primero. Si no hay goroutine libre, se iniciará una goroutine temporal para ejecutar t (). Es equivalente a len (p.sem) goroutines para formar un grupo de goroutine.Las gorutinas en el grupo procesan la lógica empresarial, en lugar de ejecutar la lógica empresarial mediante la goroutine que procesa los paquetes de red, logrando así el desacoplamiento. Una de las trampas encontradas al escribir seata-golang es que olvidar establecer taskPool provocó la misma goroutine para procesar la lógica de negocios y procesar la lógica de mensajes de red subyacente. Bloqueé toda la goroutine mientras esperaba que se completara una tarea en la lógica de negocios. Para que no se puedan recibir mensajes durante el período de bloqueo.
4. Realización concreta
Consulte getty.go para obtener el siguiente código :
// Reader is used to unmarshal a complete pkg from buffer
type Reader interface {
Read(Session, []byte) (interface{}, int, error)
}
// Writer is used to marshal pkg and write to session
type Writer interface {
// if @Session is udpGettySession, the second parameter is UDPContext.
Write(Session, interface{}) ([]byte, error)
}
// ReadWriter interface use for handle application packages
type ReadWriter interface {
Reader
Writer
}
// EventListener is used to process pkg that received from remote session
type EventListener interface {
// invoked when session opened
// If the return error is not nil, @Session will be closed.
OnOpen(Session) error
// invoked when session closed.
OnClose(Session)
// invoked when got error.
OnError(Session, error)
// invoked periodically, its period can be set by (Session)SetCronPeriod
OnCron(Session)
// invoked when getty received a package. Pls attention that do not handle long time
// logic processing in this func. You'd better set the package's maximum length.
// If the message's length is greater than it, u should should return err in
// Reader{Read} and getty will close this connection soon.
//
// If ur logic processing in this func will take a long time, u should start a goroutine
// pool(like working thread pool in cpp) to handle the processing asynchronously. Or u
// can do the logic processing in other asynchronous way.
// !!!In short, ur OnMessage callback func should return asap.
//
// If this is a udp event listener, the second parameter type is UDPContext.
OnMessage(Session, interface{})
}
Mediante el análisis de todo el código getty, siempre que logremos ReadWriter
mediante el códec de mensaje RPC, y luego implementemos EventListener
el proceso lógico especificado correspondiente a los mensajes RPC, ReadWriter
lograremos e EventLister
implementaremos el RPC del lado de inyección de cliente y servidor, las comunicaciones RPC se pueden lograr.
4.1 Implementación del protocolo de codificación y decodificación
La siguiente es la definición del protocolo seata:
Al lograr la interfaz ReadWriter RpcPackageHandler
, el mensaje de llamada del método Codec del mismo de acuerdo con el códec de formato anterior:
// 消息编码为二进制比特
func MessageEncoder(codecType byte, in interface{}) []byte {
switch codecType {
case SEATA:
return SeataEncoder(in)
default:
log.Errorf("not support codecType, %s", codecType)
return nil
}
}
// 二进制比特解码为消息体
func MessageDecoder(codecType byte, in []byte) (interface{}, int) {
switch codecType {
case SEATA:
return SeataDecoder(in)
default:
log.Errorf("not support codecType, %s", codecType)
return nil, 0
}
}
4.2 Implementación del lado del cliente
Veamos la EventListener
implementación del lado del cliente RpcRemotingClient
:
func (client *RpcRemoteClient) OnOpen(session getty.Session) error {
go func()
request := protocal.RegisterTMRequest{AbstractIdentifyRequest: protocal.AbstractIdentifyRequest{
ApplicationId: client.conf.ApplicationId,
TransactionServiceGroup: client.conf.TransactionServiceGroup,
}}
// 建立连接后向 Transaction Coordinator 发起注册 TransactionManager 的请求
_, err := client.sendAsyncRequestWithResponse(session, request, RPC_REQUEST_TIMEOUT)
if err == nil {
// 将与 Transaction Coordinator 建立的连接保存在连接池供后续使用
clientSessionManager.RegisterGettySession(session)
client.GettySessionOnOpenChannel <- session.RemoteAddr()
}
}()
return nil
}
// OnError ...
func (client *RpcRemoteClient) OnError(session getty.Session, err error) {
clientSessionManager.ReleaseGettySession(session)
}
// OnClose ...
func (client *RpcRemoteClient) OnClose(session getty.Session) {
clientSessionManager.ReleaseGettySession(session)
}
// OnMessage ...
func (client *RpcRemoteClient) OnMessage(session getty.Session, pkg interface{}) {
log.Info("received message:{%v}", pkg)
rpcMessage, ok := pkg.(protocal.RpcMessage)
if ok {
heartBeat, isHeartBeat := rpcMessage.Body.(protocal.HeartBeatMessage)
if isHeartBeat && heartBeat == protocal.HeartBeatMessagePong {
log.Debugf("received PONG from %s", session.RemoteAddr())
}
}
if rpcMessage.MessageType == protocal.MSGTYPE_RESQUEST ||
rpcMessage.MessageType == protocal.MSGTYPE_RESQUEST_ONEWAY {
log.Debugf("msgId:%s, body:%v", rpcMessage.Id, rpcMessage.Body)
// 处理事务消息,提交 or 回滚
client.onMessage(rpcMessage, session.RemoteAddr())
} else {
resp, loaded := client.futures.Load(rpcMessage.Id)
if loaded {
response := resp.(*getty2.MessageFuture)
response.Response = rpcMessage.Body
response.Done <- true
client.futures.Delete(rpcMessage.Id)
}
}
}
// OnCron ...
func (client *RpcRemoteClient) OnCron(session getty.Session) {
// 发送心跳
client.defaultSendRequest(session, protocal.HeartBeatMessagePing)
}
clientSessionManager.RegisterGettySession(session)
La lógica se analizará a continuación.
4.3 Implementación del Coordinador de transacciones en el servidor
Ver el código DefaultCoordinator
:
func (coordinator *DefaultCoordinator) OnOpen(session getty.Session) error {
log.Infof("got getty_session:%s", session.Stat())
return nil
}
func (coordinator *DefaultCoordinator) OnError(session getty.Session, err error) {
// 释放 TCP 连接
SessionManager.ReleaseGettySession(session)
session.Close()
log.Errorf("getty_session{%s} got error{%v}, will be closed.", session.Stat(), err)
}
func (coordinator *DefaultCoordinator) OnClose(session getty.Session) {
log.Info("getty_session{%s} is closing......", session.Stat())
}
func (coordinator *DefaultCoordinator) OnMessage(session getty.Session, pkg interface{}) {
log.Debugf("received message:{%v}", pkg)
rpcMessage, ok := pkg.(protocal.RpcMessage)
if ok {
_, isRegTM := rpcMessage.Body.(protocal.RegisterTMRequest)
if isRegTM {
// 将 TransactionManager 信息和 TCP 连接建立映射关系
coordinator.OnRegTmMessage(rpcMessage, session)
return
}
heartBeat, isHeartBeat := rpcMessage.Body.(protocal.HeartBeatMessage)
if isHeartBeat && heartBeat == protocal.HeartBeatMessagePing {
coordinator.OnCheckMessage(rpcMessage, session)
return
}
if rpcMessage.MessageType == protocal.MSGTYPE_RESQUEST ||
rpcMessage.MessageType == protocal.MSGTYPE_RESQUEST_ONEWAY {
log.Debugf("msgId:%s, body:%v", rpcMessage.Id, rpcMessage.Body)
_, isRegRM := rpcMessage.Body.(protocal.RegisterRMRequest)
if isRegRM {
// 将 ResourceManager 信息和 TCP 连接建立映射关系
coordinator.OnRegRmMessage(rpcMessage, session)
} else {
if SessionManager.IsRegistered(session) {
defer func() {
if err := recover(); err != nil {
log.Errorf("Catch Exception while do RPC, request: %v,err: %w", rpcMessage, err)
}
}()
// 处理事务消息,全局事务注册、分支事务注册、分支事务提交、全局事务回滚等
coordinator.OnTrxMessage(rpcMessage, session)
} else {
session.Close()
log.Infof("close a unhandled connection! [%v]", session)
}
}
} else {
resp, loaded := coordinator.futures.Load(rpcMessage.Id)
if loaded {
response := resp.(*getty2.MessageFuture)
response.Response = rpcMessage.Body
response.Done <- true
coordinator.futures.Delete(rpcMessage.Id)
}
}
}
}
func (coordinator *DefaultCoordinator) OnCron(session getty.Session) {
}
coordinator.OnRegTmMessage(rpcMessage, session)
Registre Transaction Manager, coordinator.OnRegRmMessage(rpcMessage, session)
registre Resource Manager. Consulte a continuación el análisis lógico específico.
Mensaje en el coordinator.OnTrxMessage(rpcMessage, session)
método, de acuerdo con el código de tipo de enrutamiento de mensajes a una lógica específica entre:
switch msg.GetTypeCode() {
case protocal.TypeGlobalBegin:
req := msg.(protocal.GlobalBeginRequest)
resp := coordinator.doGlobalBegin(req, ctx)
return resp
case protocal.TypeGlobalStatus:
req := msg.(protocal.GlobalStatusRequest)
resp := coordinator.doGlobalStatus(req, ctx)
return resp
case protocal.TypeGlobalReport:
req := msg.(protocal.GlobalReportRequest)
resp := coordinator.doGlobalReport(req, ctx)
return resp
case protocal.TypeGlobalCommit:
req := msg.(protocal.GlobalCommitRequest)
resp := coordinator.doGlobalCommit(req, ctx)
return resp
case protocal.TypeGlobalRollback:
req := msg.(protocal.GlobalRollbackRequest)
resp := coordinator.doGlobalRollback(req, ctx)
return resp
case protocal.TypeBranchRegister:
req := msg.(protocal.BranchRegisterRequest)
resp := coordinator.doBranchRegister(req, ctx)
return resp
case protocal.TypeBranchStatusReport:
req := msg.(protocal.BranchReportRequest)
resp := coordinator.doBranchReport(req, ctx)
return resp
default:
return nil
}
4.4 análisis del administrador de sesiones
Cliente: establezca la conexión con el Coordinador de transacciones desde la conexión a través de clientSessionManager.RegisterGettySession(session)
la conexión almacenada en serverSessions = sync.Map{}
este mapa. La clave del mapa es RemoteAddress obtenida de la sesión, que es la dirección del Coordinador de transacciones, y el valor es la sesión. De esta manera, el lado del cliente puede registrar Transaction Manager y Resource Manager con Transaction Coordinator a través de una sesión en el mapa. Consulte el código específico getty_client_session_manager.go
.
Una vez que Transaction Manager y Resource Manager están registrados con Transaction Coordinator, se puede utilizar una conexión para enviar mensajes TM o mensajes RM. Usamos RpcContext para identificar una información de conexión:
type RpcContext struct {
Version string
TransactionServiceGroup string
ClientRole meta.TransactionRole
ApplicationId string
ClientId string
ResourceSets *model.Set
Session getty.Session
}
Cuando se recibe un mensaje de transacción, necesitamos construir dicho RpcContext para la lógica de procesamiento de la transacción posterior. Por lo tanto, construiremos el siguiente mapa para almacenar en caché la relación de mapeo:
var (
// session -> transactionRole
// TM will register before RM, if a session is not the TM registered,
// it will be the RM registered
session_transactionroles = sync.Map{}
// session -> applicationId
identified_sessions = sync.Map{}
// applicationId -> ip -> port -> session
client_sessions = sync.Map{}
// applicationId -> resourceIds
client_resources = sync.Map{}
)
Por lo tanto, Transaction Manager y un Resource Manager respectivamente coordinator.OnRegTmMessage(rpcMessage, session)
y coordinator.OnRegRmMessage(rpcMessage, session)
registrados cuando el Transaction Coordinator, se almacenarán en caché en el mapa de cliente_sessions anterior applicationId, ip, relación de puerto con la sesión, (en la presente aplicación se puede almacenar en caché applicationId client_resources resourceIds map con una pluralidad de Resource Manager ) Relación. Cuando sea necesario, podemos construir un RpcContext a través de la relación de mapeo anterior. La implementación de esta parte es muy diferente a la versión java de seata, quienes estén interesados pueden aprender más sobre ella. Consulte el código específico getty_session_manager.go
.
Hasta ahora, hemos terminado de analizar el mecanismo de todo el modelo de comunicación RPC de seata-golang .
3. El futuro de seata-golang
seata-golang comenzó a desarrollarse en abril de este año, y básicamente se dio cuenta de la intercomunicación con la versión java del protocolo seata 1.2 en agosto, realizó el modo AT (coordina automáticamente el compromiso y la reversión de transacciones distribuidas) para la base de datos mysql, realizó el modo TCC y el terminal TC Utilice mysql para almacenar datos y convertir TC en una aplicación sin estado para admitir la implementación de alta disponibilidad. La siguiente figura muestra el principio del modo AT:
En el futuro, aún queda mucho trabajo por hacer, tales como: soporte para el centro de registro, soporte para el centro de configuración, intercomunicación de protocolo con la versión java de seata 1.4, soporte para otras bases de datos, implementación del coordinador de transacciones de balsa, etc., y esperamos solucionar el problema de las transacciones distribuidas. Los desarrolladores interesados pueden unirse para crear un marco completo de transacciones distribuidas de golang.
Referencia
- seata oficial:https://seata.io
- java 版 seata :https://github.com/seata/seata
- Dirección del proyecto seata-golang:https://github.com/opentrx/seata-golang
- seata-golang go estación de lectura nocturna b compartiendo:https://www.bilibili.com/video/BV1oz411e72T
Sobre el Autor
Liu Xiaomin (GitHubID dk-lockdown), actualmente trabaja en la sucursal de h3c Chengdu, es bueno en el uso del lenguaje Java / Go, ha estudiado en direcciones técnicas nativas de la nube y relacionadas con microservicios, y actualmente se especializa en transacciones distribuidas.
Yu Yu (github @AlexStocks), proyecto dubbo-go y líder de la comunidad, programador con más de diez años de experiencia laboral de primera línea en el desarrollo de infraestructura del lado del servidor, y participó sucesivamente en la mejora de proyectos conocidos como Muduo / Pika / Dubbo / Sentinel-go Actualmente se dedica a la orquestación de contenedores y trabajo de malla de servicios en el Departamento de Trusted Native de Ant Financial.