引子算法
我接触过不少编程语言,接触过各类各样的服务器端开发,Java,Go,Ruby,Javascript等语 言,Spring,Node.js,Rails等等常见服务器端框架和编程模型都有接触。这里谈一下我我的对高性能服务器端程序的一些见解,但愿给各位读 者一些认识。这片文章提到的内容也是 Coding(https://coding.net) 代码托管乃至整站都在使用的一些概念和技术。数据库
此外,阅读这篇文章,有以下几个前提:不谈硬件,不评论编程语言以及框架的好坏,不谈高级算法,可拍砖,拒绝喷子编程
Cache,Asynchronous,Concurrent
咱们一个一个来说。浏览器
Cache 翻译成中文就是缓存,台湾的叫法叫作快取,其本质是将获取缓慢或者计算缓慢的数据结果暂时存储起来,以便之后再次获取或者计算一样的数据能够直接从存储中 取得结果,从而可能提高性能的一种手段。Cache 最先是应用在计算机的 CPU 中,这篇文章不谈硬件,因此有须要了解 CPU 的缓存的同窗可自行搜索。缓存
能够想象,若是让一我的一遍一遍的从 1+2+3+4+…+99+100=? 这样去算,他加到最后发现等于5050,而这个过程耗费了他大量的时间,耗费了大量的脑力,在此期间,他可能把全部精力都放在这个计算上面而无暇顾及其余 事情。等到他累得满头大汗,加完告终果,他告诉你是 5050。没过多久,你又让他作一样的事情,我相信这家伙会不加思索的再次告诉你 5050。为何?你会笑我说,人又不是傻子,这为同窗确定记得这个结果是5050啊。服务器
但是,计算机不同,计算机就是你上面要嘲笑的那个傻子,他傻到,彻底不会记得刚在作了什么事情,他会傻乎乎的再从新算一遍告诉你结果。没错如 果你问他一万遍,这头没有脑子的机器会算一万遍的。虽然上面这个从1加到100这个例子对于一款现代化的计算机来说简直是小菜一碟,可是计算机每每面临的 计算难题是咱们人类所没法企及的。并发
Cache 就是为了来解决这个事情的,由于事情每每是这样的:你会发现一些很是复杂的过程的计算结果是可重用的,并且把这个结果暂时存储在某些地方,查找起来也是极为方便的。框架
因此,如今你理解了缓存,那能够来思考一些缓存的设计策略了。这里作一点说明,不一样的缓存策略跟具体的业务系统关系很是大,制定缓存策略须要根据具体的状况来分析。经常使用的策略:异步
不知不觉中,你有没有发现,1+2+3+4+…+99+100=5050 是个永远都成立的事实,这也就意味着,它永远不用被清除。可事实是每每是,缓存是有有效期的,例如须要缓存今天的天气状况,今天是 2014年11月16日,到了明天就是 11月17日,天气就不同了。再例如须要缓存 Coding 的最新冒泡列表,当有人发布了新的冒泡,那么这个列表就得被更新。从这个角度来看,缓存的策略又有以下常见的几种:async
嗯,既然提到了缓存的更新或者清除,那么就牵扯到缓存的更新策略。例子永远好过大段的理论:假如咱们要缓存 Coding 的冒泡列表。有这么一种策略:当用户请求时咱们检查下是否已存在这样的缓存,若是有直接返回缓存数据,不然咱们生成这个列表(计算机的计算过程),返回给 用户而且把冒泡列表(计算结果)存储起来,以便之后的用户访问时直接获取。当用户发布了一个新的冒泡的时候,咱们清除这个缓存,再有用户请求时将重复以上 过程。这是其中一种完整的缓存清除策略。另一种是,每当咱们收到一个用户发布的冒泡时,都从新构建这个缓存,用户每次查看冒泡列表都是取的缓存数据。这 两种缓存分别称之为:
关于 Cache 还有不少不少须要注意和设计上的思路和策略,这里再也不一一赘述。这些缓存在不一样的维度有不一样的策略,咱们须要根据具体的业务状况来选择合适的策略。 Coding 的不少业务中使用了上述不少种策略,例如咱们常见的分支列表和标签列表就是使用触发式失效缓存,咱们的广场项目列表就是使用主动式缓存构建。
Asynchronous 的意思是异步。什么是异步呢?就是不在第一时间告知调用者结果,告诉他我已经收到这个任务了,我会处理,处理完毕后通知你结果,若是你不是等不到结果就没法进行下去的话,你彻底能够先干别的事情。
嗯,好像我描述的比较拉杂。仍是例子:你去咖啡厅点一杯咖啡,服务员告诉你现磨咖啡须要15分钟才可作好,那么在咖啡作好以前,你不可能盯着服务员或者 咖啡师15分钟,你确定会干点别的,好比说玩手机上一下网,或者跟你女友商量下去看电影什么的,总之你不会傻乎乎等着的。等到咖啡作好了,服务员会记得 给你端过来的。这就是异步过程,你的大脑没必要为一个漫长的过程卡住,能够继续其余的事情。
服务端程序设计每每也是这样,在你等待一个很缓慢的过程的时候,若是你不是必需要获得这个过程的结果才能继续下去,你彻底能够先进行别的过程,等到那个缓慢的过程执行完毕后,它会通知你结果的。
异步已经在如今的各类编程领域有了很普遍的应用,例如 Ajax 技术,就是一种异步的手段,在浏览器和服务器交互的时候,彻底不影响你在网页上的其余操做。
异步在各类编程语言和框架中都有相应的支持,这里简单介绍一下 Javascript 的异步支持。熟悉它的人的人请无视这段。它使用回调的方式支持异步,大体意思是,A 交代给 B 一个任务,而且告知 B 任务完成后继续执行哪段程序(每每包装成一个匿名function),B执行完任务后,执行这个匿名的 function,这样来完成异步过程。在 Javascript 中大量的使用这种回调的异步方案,已经再也不局限于对一个缓慢的过程了,能够对几乎全部的过程都采用异步处理。
在服务端程序中,除了使用线程,协程,回调以外,另一种常见的异步的支持方式就是消息队列。其原理是,生产者发送消息到消息队列中,消费者从中取出消息,作出相应处理,并把结果存储起来或者经过某种方式告知生产者。
异步在不少时候能够运用现代化计算机 CPU 的多核特性和分布式计算特性,能显著的提高应用的性能,可是一个前提就是,异步的任务的结果必须是主进程进行下一步操做所不依赖的,不然主进程必须等待, 直到这个任务执行结束,拿到结果再进行下一步,这时就变成了传统的同步计算了。
异步操做在 Coding 中也有很是普遍的应用。例如当用户执行完一次 Push,Coding 须要生成一条 Push 的动态,须要清理掉相应的缓存,须要触发相关的 WebHook 等等,这些操做都是经过消息队列来异步完成的。由于这些操做很是的耗时,并且彻底不须要即时完成,因此用户在 Push 的时候等待着这些操做完成是很不合理的。异步操做在这里即展现出了其应用多核和多台服务器的优点,在某种程度上还能提高用户体验。
Golang 是 Google 2009 年发布的一门现代化语言,其语言特性对异步提供了良好的支持。这里举个例子体现一下异步的魅力:
//一个结构体 type project struct { //参数Channel name chan string result chan string } //addProject func addProject(u user, p project) { //检查用户权限 checkPermission(u) //启动协程 go func() { //获取输入 name := <-p.name //访问数据库,输出结果通道 q.result <- "add project :" + name }() } //主进程 func main() { //初始化project p := project{ make(chan string, 1), make(chan string, 1) } //某位用户 u := user{} //执行addProject,注意执行的时候还不须要告知要建立的项目名字 addProject(u,p) //准备参数 p.name <- "an-asynchronous-project" //获取结果 fmt.Println(<-p.result) }
这一段程序涉及到了 Golang 的 goroutine 和 channel,不了解的能够去查一下相关资料。
这段程序实现了 在还为准备好参数时就已经调用一个 function 。当咱们调用 addProject 的时候还不知道项目的名字,可是这彻底不影响咱们去检查用户权限。程序彻底能够一边去检查权限,一边去获取项目名字,当程序执行到不得不拿到项目的名字才 能继续的时候,它将阻塞,直到咱们告诉他项目名字。
Concurrent 的意思是并行。现代化的 CPU 每每具备多个核心,并且有些 CPU 也具备超线程能力。若是咱们能够将单个过程拆分红小的任务,交给 CPU 的多个核心,或者是分布式计算系统的多个计算节点,就能够充分利用并行计算来提高性能。前提是这些任务相互之间不要有相互依赖的关系。依然是例子:须要计 算网站上某一批用户的活跃度积分,传统的,咱们会查出这一批用户,而后写一个循环,而后轮流计算他们的积分,最后获得结果。其实每一个用户的积分的计算都是 独立的,相互不依赖,那么咱们就能够利用这一点来并行化这个计算。
下面给出一段 Coding 代码托管中的程序,这段程序是指定条件获取一个提交列表,使用了并行计算的一种 并发循环:
public List<Commit> getCommits(String objectId, String path, int offset, int maxCount) { List<String> shas = getCommitsSha(this, objectId, path, offset, maxCount); List<Commit> commits = new ArrayList<>(); if (shas != null) { List<GetCommit> getCommits = new ArrayList<>(); for (String sha : shas) { getCommits.add(new GetCommit(this, sha)); } //声明一个自适应的线程池 ExecutorService executor = Executors.newFixedThreadPool(8); List<Future<Commit>> futureList = null; //并发的调用getCommit futureList = executor.invokeAll(getCommits); executor.shutdown(); for (Future<Commit> future : futureList) { Commit commit = future.get(); commits.add(commit); } } return commits; } //Java 是一个啰嗦的语言,还要声明一个类来包装一下这个过程。 class GetCommit implements Callable<Commit> { private Repo repo; private String sha; public GetCommit(Repo repo, String sha) { this.repo = repo; this.sha = sha; } @Override public Commit call() throws Exception { return repo.getCommit(sha); } }
这段程序是一个并发循环的例子,例子中须要根据一些参数查询到 Commit 的列表,而 repo.getCommit 这个过程彻底不须要一个一个轮流查询,由于他们是彻底独立的,因此可使用 Java 的 Cocurrent 包来作并发循环,充分利用多核来尽快获得执行结果。
关于高性能服务器程序须要关注的点还有不少,这里只是简单的介绍了下三个利器 (Cache,Asynchronous,Concurrent)。而即使是这三个利器,个人介绍也只是冰山一角,可是请相信你看懂了我介绍的这些东西, 从新去思考服务端编程会得到很多收获的。
这三者也是相辅相成的关系,不少时候都是配合着使用才能起到很好的效果。异步和并行在某种程度上是有重叠的,而咱们常用异步的方式去主动构建缓存。
最后再给一些小提示: