Go微服务实战1:为什么是go

现在越来越多的公司使用go语言做为后台开发的首选语言,另一方面,从架构上,微服务是目前后台开发中广为使用的模式,go语言,微服务,一个干柴,一个烈火,他们二者结合在一起能摩擦出什么样的效果值得好好研究研究。

本系列的文章,将围绕micro微服务框架展开,目前go语言的开源微服务框架有很多,比如micro和腾讯的tars go等,其中tars go相对而言配套更加完善,有可视化的管理平台,因此使用的人更多。

本系列文章的目标受众:有一定编程基础,想入门后台开发的同学。

后续我还将推出基于kubernetes的微服务实战系列,当切换到kubernetes之后,你会发现诸如micro,tars go之类的第三方框架将彻底消失,留给我们一个干净又无比清爽的世界,值得期待。

如果您觉得阅读完文章后有所得,可以动动手指点个关注,我们一起学习,一起进步!

好了,言归正传,让我们开始踏上go语言微服务开发之旅吧,这篇文章我们先从选型说起,聊一聊为什么越来越多的开发者选择go做为后台开发语言。

1 为什么是go

很多公司已经或准备开始用go语言开发后台,B站,字节老早就在用go了,而以C++为主的腾讯,有很多业务也逐步在向go过渡。

哲学告诉我们世界是由因果驱动的,任何事情的发生其背后都有驱动因素,那么用go开发后台的“因”是什么呢?

1.1 天然的并发支持

了解go语言的同学一定知道协程这个概念,相比线程而言,协程具有更高的性能,对并发的支持非常好,可以毫不夸张的说,即便你是一个刚入门的后台开发小白,用go语言原生的http server也足以写出一个能够支撑起创业公司前期业务的服务来。

我们简单回顾一下并发编程的历史:

首先出场的选手是多进程,可是进程这东东它的创建和销毁的开销实在是太大,很难满足并发量大的场景,不信你在电脑上使劲fork看能撑多久;

进程不行就换线程,相比于多进程,线程只占用很少的堆栈空间,并发问题在一段时间内似乎得到了解决,可是随着并发规模的不断增大,频繁创建和销毁线程的开销也开始变得难以接受,怎么办?用线程池呗,把线程保存起来以避免频繁创建和销毁,是个好主意,可是线程切换的开销又来了,这可真是按下了葫芦漂起了瓢,麻烦的事儿一桩桩啊!

最后只有祭出杀器了,epoll和协程,倚天屠龙在手,天下我有。

epoll就不说了,单讲协程,让先来回答4个灵魂拷问,请听题:

1.1.1 为什么协程比线程快

很多背过题的小伙伴可能要说了:因为协程不需要从用户态切换到内核态呗。没错,但是光知道答案还不行,我们还得在探究的更深入一点:从用户态切换到内核态到底发生了什么?这种切换的性能影响到底有多大?

1.1.2 用户态到内核态的切换到底有多慢

来看网上一个有着强烈好奇心的朋友做的benchmark测试,这个测试很简单,返回当前用户的uid,一个版本用系统调用实现,一个则是普通的实现方式。

以下是系统调用版本的实现:

#include <unistd.h>

#define MAX 5000000

int main(int argc, char** argv) {
  
  int i = 0;
  for (; i < MAX; i++) getuid();

  return 0;
}

很简单,就是通过系统调用getuid获取用户id,这本版本最后的执行时间花费了11s

再看另一个不用系统调用的版本:

#define MAX 5000000

int _getuid() {
    return 1001
}

int main(int argc, char** argv) {

    int i = 0;
    for (; i < MAX; i++ ) _getuid();

    return 0;
}

 这个版本只花了0.15s。

11s VS 0.15s,几乎是100倍的差距,真是不测不知道,一测吓一跳,不知道你看到这个结论时是何感受,反正我的下巴已经掉到了地上。

1.1.3 为什么用户态到内核态的切换如此之慢

当调用了系统调用后发生了什么呢?

系统调用好比是从用户态迈进内核态的大门,当调用了系统调用函数后,将触发0x80软中断,而提到中断,脑海里有没有浮现出当年微机原理课程考试前老师给划的重点:保存现场 -> 中段服务程序 -> 恢复现场 -> 中断返回,这就是慢的根本原因。

1.1.4 为什么线程切换需要从用户态进入内核态

假设有两个线程A和B,我们知道CPU是通过时间片来调度线程的,现在假设A的时间片用完了,此时需要把线程A的现场保存下来,所谓保存现场无非就是保存诸如PC,栈指针等寄存器的值,当现场保存完成后,需要给B分配CPU时间片,将B调度起来,因为涉及到CPU,在用户态是没有权限触碰硬件的,于是只能通过系统调用一头扎进内核...

回到协程上,协程就不存在用户态到内核态的切换开销,对于协程而言,最主要的就是在被中断时保存现场和再次被调度时恢复现场,这些现场就是一些寄存器值,可以通过汇编语言进行替换。

现在找到了使用go的第一个“因”:如闪电般迅捷的协程

1.2 内存占用低

如果你稍加留意的话会注意到用go语言编写出来程序内存占用会比Java低不少,或许你会说那是因为我项目中依赖的东西多,这或许是个原因,但是不是事实还得通过数据来说话,我们来看一个关于内存的benchmark测试:

内存占用 go vs java
regex-reduxfannkuch-reduxreverse-complementspectral-normmandelbrotn-bodyfastapidigitsk-nucleotidebinary-tree
go324200231615602282208341921604114808808160328392774
java9856963519267092439304691363584444620365523569041722848

从数据上看,很明显第二个回合依然是go完胜java,同样的算法和数据量,go的内存占用比java低几个量级,此事必有蹊跷,元芳你怎么看?

下面这篇文章非常深入的分析了造成go和java内存差异的原因,强烈推荐大家阅读一下:

go/java内存占用分析

这篇文章的结论总结起来有下面这么几点:

1.2.1 面向对象 vs 非面向对象

在java中一切皆对象,而一个对象在java中的内存布局是下面这个样子的:

| 18 bit unused | 4 bit marks | 42 bit object address | 32 bit class index | value |

光是对象头部就要固定占用12个字节。

而go不是面向对象的语言,同样一个32位整数,java里需要96 + 32 = 128位,而go就能省掉12字节。

1.2.2 JAVA虚拟机会带来额外的内存开销

JAVA的运行时包含解释器,JIT,垃圾回收器,而go只有一个垃圾回收器,这也会造成更多的内存占用。

1.2.3 并发模型

java的并发模型是基于线程的,go是基于协程的,在Java中每个线程默认需要消耗1M的空间,而Go的协程默认只需要2Kb,在并发量大的情况下,这是一个非常可观的差距。

1.2.4 堆栈利用效率

go会优先把值分配在堆栈上,这样的好处就是很多小对象就会随着函数的调用被释放掉。

Tips:还记得为什么不推荐在函数中返回局部变量的指针吗?虽然在go中返回局部变量的指针不会像C/C++那样引发不可预知的结果,但是却会造成内存逃逸:编译器会在堆上给变量分配内存空间。

java虽然也有类似的逃逸分析,但因为java是面向对象的,逃逸分析无法做到像go一样堆栈优先,这也是造成内存差异的一个重要原因。

1.2.5 反射的实现机制

java语言的很多框架几乎都在大量的使用着反射,然后反射的结果又用hashmap来缓存,这是极度消耗内存的行为。

go语言也有反射,很多框架也不可避免的使用反射,但是java又吃了面向对象的亏,因为go是面向interface和值的,它的反射模型非常简单,反射过程中生成的对象数量比Java的反射要少的多。

现在使用go的第二个“因”也找到了:从娘胎里出来就带有的低内存消耗

目前已经探索到的两个使用go的因素可以说已经足够的吸引人了,大并发支持,低内存消耗,这是很多java开发者孜孜不倦,绞尽脑汁寻求优化的点,go的设计者从语言层面就帮我们把障碍扫除了。

1.3 简单易上手

最后一个使用go的原因非常现实:简单易上手。

假如你进入了一家初创公司,因为资金有限,你得一个人把开发的活都给做了,而你此前从未接触过后台开发,那么选用go,用类似gin的框架你大概只需要几天的时间就能写出足以应付项目前期并发量的后端服务。

而如果用java,你不得不面对spring,还得了解一下netty,等你把环境搭起来,调通第一个程序,没有一点天赋的话估计怎么着也得半个月吧。

go就不一样了,它的语法足够简单,安装依赖软件分分钟的事,这年头时间就是生命,效率就是金钱,学习成本低,效果又出奇好的东西,为什么不去用呢?

2 总结

现在我相信你已经对为什么使用go开发后台有了一定的认知,简单总结一下:

  • go语言的并发模型是协程,相比基于多线程的java,协程因为避免了频繁从用户态切入内核的开销,具有更好的性能,对并发的支持更好。
  • go语言写出来的程序,相较于java,内存占用更低,这源自于语言本身的设计。
  • go语言简单易学,适合新人快速上手。

最后,如果觉得阅读完本文后您有所收获,可以动动手指关注作者哦:)

思考:

线程池解决了什么问题?

什么原因导致了从用户态切入内核态的性能损失?

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值