在目前这套项目架构诞生初期,基于当时的游戏类型和项目需求,架构做得相对简单,设计上尽可能通过goroutine而不是节点来并发,节点间用ETCD做服务发现,用gRPC做节点通信,在单向依赖,弱藕合的情况下,基本能够满足需求。对于个别强耦合的节点交互,使用gRPC Stream来建立双工连接。

随着游戏类型和业务需求的变更,跨服功能增多,节点划分越来越细,藕合越来越重,网络拓扑也越来越复杂。gRPC Stream不再能很好地胜任。

因此我们考虑用一套新的节点交互方案,大概有两个思路:

  1. 写一套完备的TCP网络库(包含服务发现,自动重连,编解码,心跳,流控等),用于统一节点间甚至Gateway与Client间的网络交互
  2. 使用MQ解耦集群内节点交互,将网状网络化为星形网络,简化网络拓扑
阅读全文 »

2022.1.22 更新: 最近一年,Go泛型已经从草案,过渡到提案,并开始实现。Go1.18实现了初版泛型,最终方案相较之前的泛型草案,将类型列表约束(type list in constraint)进一步丰富完善为类型集约束(type sets of constraints)的概念,本文内容已随最新文档更新。

之前我在编程范式游记中介绍了OOP中的子类化(subtype,也叫子类型多态subtype polymorphism)和泛型(generics,也叫参数多态parametric polymorphism或类型参数type parameters),关于两者的区别和比较可以参考那篇文章,在其中我吐槽了Go目前对泛型支持的匮乏,Go泛型最初在Go 2中讨论,目前已经在Go1.18中正式实现,随着Go泛型设计和实现的细节也越来越清晰,我们从最新的Go泛型文档来了解下Go泛型设计上有哪些考量和取舍。

PS. 虽然Go官方文档仍然沿用社区惯用的”泛型(generics)”术语,由于主流语言都有自己不同的泛型支持,如Java基于编译期类型擦除的泛型(伪泛型),C++的图灵完备的模板机制(支持模板元编程的真泛型)等,为了避免概念混淆,将Go泛型理解为类型参数(type parameters)更精确,它不支持C++那样灵活的模板元编程,但比Java这种运行时擦除类型信息的补丁实现更优,另外,关于运算符泛型,Go也提出了一套新的解决方案。

1. 最简原型

先从最简单的泛型定义开始:

1
2
3
4
5
6
7
8
// Define
func Print[T] (s []T) {
for _, v := range s {
fmt.Println(v)
}
}
// Call
Print[int]([]int{1, 2, 3})

语法上和其它语言泛型大同小异,泛型的本质是将类型参数化,Go中用函数名后的[]定义类型参数。以上声明对C++开发者来说非常亲切的(只是换了一种语法形式),实际上这在Go中是错误的泛型函数声明,因为它没有指明类型参数约束(constraints)。

阅读全文 »

在之前的博客中几次简单提及过给GS做测试,关于测试的必要性不用再多说,但在实际实践过程中,却往往会因为如下原因导致想要推进测试规范困难重重:

-. Q1: 写测试代码困难: 代码耦合重,各种相互依赖,全局依赖,导致写测试代码”牵一发而动全身”,举步维艰
-. Q2: 测试时效性低: 需求变更快,数值变更频繁,可能导致今天写好的测试代码,明天就”过时”了
-. Q3: 开发进度紧: 不想浪费过多时间来写测试代码,直接开发感觉开发效率更高

要想推进测试规范,上面的三个问题是必须解决的。这里简单聊聊我们在Golang游戏后端中的测试实践和解决方案。我们在GS中尝试的测试方案主要分为四种: 单元测试,集成测试,压力测试,以及模拟测试。

阅读全文 »

之前被同事安利了很多次Rust,周末没事去Rust官方文档学习了下,记录一些对Rust语言粗浅理解。

一. 所有权系统

要说Rust语言的核心优势,应该就是运行效率+内存安全了,这两者都与其独树一帜的所有权系统有关。要谈所有权系统,GC是个不错的切入点,众所周知,编程语言GC主要包含两种: 手动GC和自动GC,它们各有利弊,总的来说是运行效率和内存安全之间的权衡取舍。而Rust则尝试两者兼顾,Rust的GC,我将其理解为半自动GC或编译期GC,即开发者配合编译器通过所有权约束来明确变量的生命周期,这样Rust在编译期就已经知道内存应该何时释放,不需要运行时通过复杂的GC算法去解析变量的引用关系,也无需像C/C++让开发者对各种内存泄露、越界访问等问题如履薄冰。这也是Rust敢号称可靠的系统级编程语言,运行时效率叫板C/C++的底气来源。

阅读全文 »

一. Go GC 要点

先来回顾一下GC的几个重要的阶段:

Mark Prepare - STW

做标记阶段的准备工作,需要停止所有正在运行的goroutine(即STW),标记根对象,启用内存屏障,内存屏障有点像内存读写钩子,它用于在后续并发标记的过程中,维护三色标记的完备性(三色不变性),这个过程通常很快,大概在10-30微秒。

Marking - Concurrent

标记阶段会将大概25%(gcBackgroundUtilization)的P用于标记对象,逐个扫描所有G的堆栈,执行三色标记,在这个过程中,所有新分配的对象都是黑色,被扫描的G会被暂停,扫描完成后恢复,这部分工作叫后台标记(gcBgMarkWorker)。这会降低系统大概25%的吞吐量,比如MAXPROCS=6,那么GC P期望使用率为6*0.25=1.5,这150%P会通过专职(Dedicated)/兼职(Fractional)/懒散(Idle)三种工作模式的Worker共同来完成。

阅读全文 »

最近做的优化比较多,整理下和Go内存相关的一些东西。

一. 不要过早优化

虽是老生常谈,但确实需要在做性能优化的时候铭记在心,个人的体会:

  1. first make it work, then measure, then optimize
  2. 二八原则
  3. 需求变更快
  4. 对性能的主观直觉不靠谱
阅读全文 »

简单谈谈我们最近是如何给GS做压测的。

1. 压测机器人

压测机器人需要满足如下几个条件:

  1. 异步请求: 异步才能模拟真实的客户端请求和压力
  2. 数据同步: 像客户端一样缓存和处理服务器响应数据,这样才能做好有效请求和可重入
  3. 可重入: 机器人应该可以在任何时候关闭/重启,而不应该假设初始状态(比如只有注册的时候能跑)
  4. 随机性: 机器人行为尽可能随机分布,并且每次重启重新初始化随机种子
阅读全文 »

这段时间学习OOP对语言和编程范式有一些新的理解,之前系统整理过函数式编程,因此先从OOP谈起。我们先回顾下面向对象(OOP)的核心思想:

  1. 所有的值都是对象
  2. 对象与对象之间通过方法调用(或者说是发消息)进行通信
  3. 对象可以有自己的私有字段/状态,只有对象的方法可以访问和更新这些字段
  4. 每个对象都是一个类(Class)的实例,类定义了对象的行为(内部数据和方法实现)

与函数式的”一切皆函数”一样,OOP也有一个宏大的目标”一切皆对象”。

阅读全文 »