EddyZhou's blog

我会在这里记录我的生活,希望一切变美好

上传速度优化

上周把服务器和客户端都部署到了阿里云服务器,因为在频繁修改,每天需要发布很多次。客户端做了增量更新,除了第一次上传很慢,之后都很快。服务端是打成了一个jar,有40M左右,每次上传需要半个多小时,慢到实在是不能接受。周四晚上做了一下优化,现在每次上传只需要两三分钟。

优化的核心思想是增量而不是全量上传,但是打成一个jar之后,rsync还是会全量上传,所以想直接上传class文件,然后在服务器上用jar打包,这样就可以增量上传了。看了下sbt assembly的选项,没看到有选项支持不打成jar。所以先在本地把jar解压:

1
2
3
rsync-aliyun-server: server/target/server-assembly-0.3.$(REVISION)-SNAPSHOT.jar server/target/server-assembly.jar
                     mkdir -p server/target/server-assembly
                     cd server/target/server-assembly && jar -xvf ../scala-2.10/server-assembly-0.3.$(REVISION)-SNAPSHOT.jar

然后把class文件上传服务器(增量上传):

1
rsync --links -avz --no-whole-file server/target/server-assembly user@serverIp:/xxx/xxx/jars/

再在服务器上用jar命令打包:

1
ssh user@serverIp 'cd /xxx/xxx/jars && jar cvf server-assembly-lastest-SNAPSHOT.jar server-assembly && ln -s -f -T server-assembly-lastest-SNAPSHOT.jar server-assembly.jar'

有状态服务器的热更新

这两天把服务器的热更新实现了下。对于无状态服务器要做热更新相对简单,如果数据在别的进程,简单粗暴的重启也是可行的,我个人也更倾向于这么做。在上一家公司,我们就常常重启AppServer,数据都在DataServer。甚至在做副本Server的时候,为了做到可随时重启,在停服的时候我把所有的组队数据和刷怪进度保存到了Memcached,重启的时候会先从Memcached读数据,从ProfileServer把玩家数据拉过来,恢复度队伍数据和刷怪进度,这样玩家会感觉到卡一下,但影响不大。现在我们的数据库VinyStorage虽然在架构上是独立的,但是物理上却是与业务逻辑同进程,这导致我们不能常常重启服务器(因为停服的时候VinyStorage把内存的数据写到磁盘可能需要较长时间)。而且我们在内存中维护了很多份房间数据,我们只希望热更新Services,而不去更新Models。所以我们不能用Tomcat等容器那样定时去检测指定目录的Class是否有修改的做法,当然我们是用scala写服务器,Nginx那样fork进程的做法更不可行.

比较适合我们需求的做法是用正则是去匹配那些类需要热更新,需要热更新的类用自定义的ClassLoader去加载,不需要热更新的类还是用SystemClassLoader去加载。自定义的OverridenClassLoader代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
final class OverridenClassLoader(
  urls: Array[URL],
  parent: ClassLoader,
  overridenClasses: Seq[Pattern] = Seq(Pattern.compile(""".*""")),
  excludeClasses: Seq[Pattern] = Seq.empty,
  overridenResources: Seq[Pattern] = Seq(Pattern.compile(""".*""")),
  excludeResources: Seq[Pattern] = Seq.empty) extends URLClassLoader(urls, parent) {

  override final def getResources(name: String) = {
    if (overridenResources.exists { _.matcher(name).find } &&
        !excludeResources.exists { _.matcher(name).find }) {
      findResources(name)
    } else {
      super.getResources(name)
    }
  }

  override final def getResource(name: String): URL = {
    if (overridenResources.exists { _.matcher(name).find } &&
        !excludeResources.exists { _.matcher(name).find }) {
      findResource(name)
    } else {
      super.getResource(name)
    }
  }

  override protected final def loadClass(name: String,
                                   resolve: Boolean): Class[_] = {
    if (overridenClasses.exists { _.matcher(name).find } &&
        !excludeClasses.exists { _.matcher(name).find }) {
      getClassLoadingLock(name).synchronized {
        {
          findLoadedClass(name) match {
            case null =>
              findClass(name)
            case c => c
          }
        } match {
          case null =>
            null
          case c =>
            if (resolve) {
              resolveClass(c)
            }
            c
        }
      }
    } else {
      super.loadClass(name, resolve)
    }
  }
}

以下是创建OverridenClassLoader的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val urls = getClass.getClassLoader match {
  case systemClassLoader if systemClassLoader == ClassLoader.getSystemClassLoader =>
    val classpath = System.getProperty("java.class.path")
    classpath.split(File.pathSeparatorChar) map {
      new File(_).toURI.toURL
    }
  case urlClassLoader: URLClassLoader =>
    urlClassLoader.getURLs
  case _ =>
    sys.error("Cannot find current classpath.")
}

new OverridenClassLoader(
  urls,
  getClass.getClassLoader,
  Seq(
    Pattern.compile("""^com\.dongxiguo\.rftw\.roomServer\.Administer$"""),
    Pattern.compile("""^com\.dongxiguo\.rftw\.roomServer\.services\."""),
    Pattern.compile("""^com\.dongxiguo\.rftw\.utils\."""))
  overridenClasses = ,
  excludeClasses = Seq.empty,
  overridenResources = Seq.empty,
  excludeResources = Seq(Pattern.compile(""".*""")))

需要注意的是,需要用OverridenClassLoader来加载入口Main,要不然热更新之后旧的service还被main引用回收不掉,会造成内存泄漏,同样需要被卸载的class需要用OverrideClassLoader来加载,因为只有classLoader回收,加载的class才会被卸载。OverridenClassLoader加载类可以引用SystemClassLoader加载的类,但SystemClassLoader加载的类绝不可以引用OverridenClassLoader加载的类。

功能禁用的一点思考

因为要参加阿里云开发者大赛,而阿里又没有social api可用,最近两天同事在做自己的用户系统(悄悄吐槽一下,明显过度设计了,我原以为一个下午的活做了两三天),在频繁的改入口,为了少与他代码冲突,我把重构热更新往后推了两天,等他弄完了之后我再改。然后在想可以利用这段时间把功能禁用重新实现下。

功能禁用对于游戏来说还是很重要的,比如有玩家反馈某个功能(比如交易)有问题,可以先把这个功能禁用掉,等定位到问题,修复之后重新发布,或者确定没有问题再把这个功能恢复。我们的服务端逻辑是基于Protocol Buffers的RPC暴露接口,每个子系统对应一个Service。之前为了赶进度,用了最简单最容易想到的方式来实现,就是在每一个service加一个functionDisable实例变量,每个接口方法都先判断这个变量是否为true,如果为true就返回给客户端一个该功能已被禁用的应答。这个方案的问题在于这是一种侵入式的做法,需要写一些重复的代码,而且容易写漏。之前确定的一个非侵入式方案是:如果需要禁用一个Service,应该用在外部把注册的Service换成一个用java.lang.reflect.Proxy创建的dummy Service,而不是在每个Service上硬编码禁用的变量。下班前我又想到一个方案,思路跟第一种差不过。 定义一个trait,该trait只有一个是否禁用功能的变量:

    trait FunctionControl {
        var functionDisable = false
    }

每个Service都extends FunctionControl,然后用宏重新生成Service, 重写每一个接口方法,先判断该功能是否禁用,然后调用现有的逻辑。相当于在每一个方法上加了一层代理,就像下面这样:

    def startProxy(controller: RpcController, request: StartRequest) {
        if (functionDisable) {
            calleeController.setFailed(CommonError.FunctionDisable.functionDisable)
            return
        }
        start(controller,request)
    }

这样就不用写很多重复的代码了。