Linux服务器上下载并安装tomcat

⭐ 我的网站: www.mengyingjie.com ⭐ 下载安装tomcat前,先下载安装JDK:Linux服务器 下载并安装 JDK 1下载tomcat:下载地址 2、上传文件,并解压:(如何向服务器里上传下载文件) tar -zxvf apache-tomcat-9.0.19.tar.gz3、加入tomcat的环境变量(不太熟悉vi操作的可以去学习下)安装完成后需要配置一下环境变量,编辑/etc/profile文件: vi /etc/profile在文件尾部添加如下配置: export CATALINA_HOME=/wocloud/tomcat_cluster/tomcat1/apache-tomcat-9.0.19编辑完成后按esc后输入:wq保存退出,最后一步就是通过source命令重新加载/etc/profile文件,使得修改后的内容在当前shell窗口有效: source /etc/profile4、启动tomcat,进入/wocloud/apache-tomcat-9.0.19/bin(我直接安装在了wocloud目录下,如果安装路径不一样,按照自己的路径)执行:./startup.sh,出现如下图片则启动成功。此时还是不能访问的tomcat主页的。 5、然后我们需要在阿里云服务器管理中开发8080和8009端口。 这个时候可以用你本机上的浏览器访问一下,如果可以访问,跳过第6步。不能访问进行第6步。6、配置防火墙在CentOS 7中引入了一个更强大的防火墙——Firewall。打开firewall systemctl start firewalld我们需要在Firewall中开启8080和8009端口(这里的两个端口是tomcat默认的访问tomcat服务器的端口,如果自己改变了端口,要按照自己修改的端口),也就是将8080和8009端口加入到zone(Firewall的新特性,简单讲它的作用就是定义了网络区域网络连接的可信等级)中。命令如下: firewall-cmd --zone=public --add-port=8080/tcp --permanentfirewall-cmd --zone=public --add-port=8009/tcp --permanent这样就成功的将8081端口加入了public区域中,permanent参数表示永久生效,即重启也不会失效,最后不要忘记更新防火墙规则: firewall-cmd --reloadOK,下面看一下public区域下所有已打开的端口,命令如下: firewall-cmd --zone=public --list-ports7、这个时候,就可以通过服务器的外网ip或者域名来访问tomcat了.遇到此类问题,但看了文章还是未解决,评论或加 QQ:781378815

September 8, 2019 · 1 min · jiezi

Linux服务器上搭建javaweb环境

⭐ 我的网站: www.mengyingjie.com ⭐ javaweb环境搭配为:tomcat9.0.19+jdk8u211+ mysql一、主要分为以下步骤:1.服务器购买,购买地址2.下载并安装jdk3.下载并安装tomcat4.下载并安装mysql,用Navicat远程访问5.将项目war包放到tomcat的webapps目录下即可二、可能遇到的问题:1.不加项目名或8080端口号,访问tomcat上的项目2.如何向服务器里上传下载文件3.使用xshell登陆时密码框为灰色遇到此类问题,但看了文章还是未解决,评论或加 QQ:781378815

September 8, 2019 · 1 min · jiezi

laravel-Hui-基础后台管理系统

laravel_quick_admin项目地址:https://github.com/tsmliyun/l...项目实例: 背景起这个项目的初衷是,对于一个后台管理系统,登陆、注销、权限管理等都是些公用的模块,完全可以封装成一个基础项目,每次新的项目基于基础项目上开发即可,节约时间,提高开发效率。 功能模块登陆找回密码修改密码注销管理员管理权限管理角色管理支持多语言代码模块routecontrollerservicemodellogrequestRepository (关于这个仁者见仁智者见智吧)项目搭建比较简单,主要以下几步 composer install修改.env文件相关配置执行laravel_quick_admin/laravel_quick_admin.sql文件中的sql语句展望后续会更新出一版 前后端分离的基础后台框架,敬请期待。 感谢laravel -- 艺术家最爱的框架 H-ui -- 轻量级前端框架

May 21, 2019 · 1 min · jiezi

『后台公论』卷首:何谓后台?

我们整天说着前端,客户端,后台。到底什么才是后台?曾经和某网友聊天:你已添加了XXX,现在可以开始聊天了。我:你好XXX:你也好我:你是做什么的呀?XXX:我在酒吧上班,做前台的。我:哦哦,那咱俩差不多,我做后台的。前台后台前台(前端)后台,在英语中即:Front-End,Back-End。广义上的前端包括客户端(PC、Android、IOS等),后台即通常意义上的Server。没错,就是在互联网诞生之初即存在的C/S架构。服务的提供方成为Server,一般是一个偏中心化的服务集群。客户端Client为用户接入服务的控制重点。C/S架构有明显的主次之分,这与后来产生各类种子(BT、电驴)下载技术的P2P对等网络相对应。在P2P网络中不区分C或S,每个节点既是C也是S,这便是对等(Peer)网络。言归正传,后台一词描述的还是Server这一概念,不过由于网络规模的增大,数据量的攀升,Server的后台架构也变得越来越复杂,分层越来越多,早已不是简单Server一词能够囊括的了。本专栏文章始于C++后台架构,却又不仅限于此,会穿插对比各语言的通信模型以及语言无关的各类概念。这里的后台开发指的就是Linux上的C++编程。首先澄清一点,很多人(比如我以前)对后台开发的误解,通常人们说前端后台,后台就马上联想到web后台,java、php和各种web框架横飞的既视感。所以大学的时候,当我看到腾讯招聘后台工程师,技能要求是C++也满是狐疑。白马非马其实web后台属于后台,但后台却不只有web后台。两者应该是包含与被包含的关系。提到后台,通信是永远的主题。通常我们谈到Python的Django,PHP的Think PHP、Yii框架所涉及到的开发知识,都是聚焦于展现和逻辑。而屏蔽了底层通信的细节,这是框架之利。但也削弱了开发者对于后台达到一切尽在掌握的一些可能性。从网络协议的角度分析,web后台聚焦的是 HTTP。web后台可以看作是一个后台架构中最靠前的东西,它解析了HTTP请求,然后层层转发给了后面整个分布式系统的许多组件,并调用他们的服务。这一层可被称之为“接入层”,C++语言进行接入层的实现,一般就是通过 CGI了,CGI这被教科书都写进历史的技术,相信很多人都不齿为用。但其实不管是 Java的Servlet,C#的 WCF,或是 Python的 WSGI,Ruby的 Rack多多少少都是受CGI影响而演化而来。行远自迩,学习了解CGI,并不是浪费时间。除了HTTP,企业内部主机之间绝大部分是自定义协议,而这些协议多半是在TCP 或 UDP之上实现应用层协议。这个层面上来说C++后台关注的是socket编程,由于C++本身并没有官方的Socket通信的库,其实这里使用的一般也就是Linux C语言的网络编程。C和C++各自的拥趸们,争与不争,它们就在那里。天下大同编程语言很重要,一门语言通常意味着的是一个技术栈,比如C++的技术架构的后台和Java的后台,相信绝不仅仅是语言的不同,各种组件以及编程的模式都差别很大。但其实时代发展到如今,编程语言之间的差异性却又不那么重要了。在当代规模的大型网络的架构中,绝不仅仅是通过某门语言自身的特效就能解决性能瓶颈的,对于架构的演进,逐渐趋于大同。与前端技术的百花齐放不同,后端技术相对落寞,甚至不乏炒冷饭的嫌疑。在Web Service逐渐式微之后,各式RPC又被炒了起来。在豪情万丈的 CORBA在新世纪由于J2EE的出现而逐渐雪藏多年之后,Facebook推出的 RPC框架 Thrift与 CORBA相比又是何其相似。当然,炒冷饭本身不是目的,不管黑猫白猫,能抓老鼠的就是好猫。旧技术在不停推陈出新后确实能焕发出新的生命力。时光荏苒,新概念层出不穷。不管是偏向于Web层的概念 Restful API还是相对繁重的分布式概念 SOA,都在趋向于接口的解耦和服务化,通过组合不同服务即可快速搭建出新业务。回想起WS(Web Service),让人唏嘘。理论上讲 WS属于 SOA,但最终走向衰落。到底是 WS生不逢时,还是新时代服务化的概念在因为弥补了 WS的缺点才得以焕发新生?这样的问题没有答案。“是耶非耶,化为飞蝶”。唯一可以肯定的是,后台技术在润物无声中不停的发展,进步。

March 31, 2019 · 1 min · jiezi

EOS钱包区块链核心业务开发详解

区块链钱包是什么?很多人会把它理解为微信支付宝钱包等,然而区块链钱包里没有数字货币,数字货币存储在区块链上,钱包作为公私钥的管理工具,用户通过钱包与区块链平台上的DApp 进行交互。区块链钱包对于区块链而言好比浏览器对于互联网一样。早期人们对于在浏览器上输入url和域名访问互联网都很陌生,但现在浏览器已经成为互联网的重要入口,被几十亿人使用。在区块链上也将会发生同样的一个演变过程,对大多数人来说,现在区块链是陌生的,随着区块链用户从数百万人激增到数千万人,那么许多团队对钱包的这个入口战略资源的争夺将比互联网时代 的浏览器更加激烈。在区块链领域,钱包毫无疑问有举足轻重的地位,很容易理解区块链钱包的应用价值:作为支付的入口随着闪电网络、雷电网络等链外支付以及分片、子链等技术的成熟,未来一旦数字代币支付成为主流的支付方式之一。那么钱包作为入口,就有很大的想象空间。作为资产管理的入口目前比特币、以太坊、EOS等公链越来越多,协议和应用越来越多,token也越来越多,中心化交易所、去中心化交易所、量化交易等都在发展。现在,一些钱包提供了理财的功能,一些长期价值投资的用户把代币存入钱包进行理财。作为交易的入口对于用户来说,在钱包就可实现快速的交易。钱包由于沉淀了很多用户的数字资产,当用户需要进行交易时,钱包与一些去中心化或中心化的交易所结合,用户输入自己的理想价格,可以实现尽快的撮合交易。作为DApp市场入口随着公链的成熟,尤其是EOS、以太坊等区块链基础设施的逐步完善,一些游戏类、金融类、社交类、泛娱乐类的DApp应用逐步发展起来。随着成千上万,甚至是几十万上百万的DApp,用户需要有一个地方去发现和下载。对于钱包来说,DApp市场入口绝对是最具想象力的前景。为什么要了解EOS钱包?有着区块链3.0之称的EOS拥有庞大的用户群体,自2018年6月份主网上线以来,注册帐户已经达到60万,而算上没有EOS帐户但在交易平台持有EOS代币的用户,可能这个数字已经超过百万:在EOS平台上开发的DApp如雨后春笋般纷纷出世,主网上现在跑的DApp已经超过了200个,其活跃量、交易量也早已超越了以太坊。这些开发团队以及个体开发者选择基于EOS开发,首先是EOS网络对开发者友好,适宜DApp应用程序开发;其次开发者对于EOS生态的未来有信心。钱包作为数字货币资产的存储和Dapp的超级流量入口,其市场需求较大,创建和管理钱包是进入区块链领域的必修课。因此我们推出本课程,自己来开发一个EOS钱包,旨在帮助区块链用户 和应用开发者全面快速地掌握区块链钱包开发的知识技能与业务流程。课程项目简介课程项目是一个手机EOS钱包,最终的实现效果如下图所示:用户可以导入自己的账号,也可以创建新的测试网账号,可以在钱包的多个账号间切换活动账号。一旦选中的当前活动账号,用户就可以查看自己的资产总览信息,也可以向其他账号转账,或者浏览自己的转账历史记录。钱包也提供了DApp开发者关心的资源管理功能。使用钱包可以购买或者出售内存资源,也可以抵押EOS获取CPU或者NET资源。作为区块链的入口,我们的钱包不仅提供了管理自己EOS账号的能力,还可以提供更多的增值服务,例如DApp推荐、市场行情、新闻动态等。课程项目技术栈概述本课程项目采用NodeJS的全栈式开发模式,基于npm+webpack的工作流,为了顺利地完成本课程的学习,你应该对以下语言/技术有一些了解:本课程采用Webpack把项目当做一个整体,从一个给定的主文件(如:index.js)开始找到项目的所有依赖文件(JavaScript,CSS和Fonts以及Image等等),通过合适的loaders处理它们,最后打包为一个浏览器可识别的JavaScript文件。本课程使用Facebook的Web App解决方案React技术栈(react+redux+react-router)以及基于React实现的UI框架Antd-Mobile,帮助学员快速完成前端H5页面的开发并提供给用户优质的用户体验。Eosjs是访问EOS区块链的JavaScript库,提供了大量简单易用的EOS的HTTP API封装方法, 其作用就像web3.js对于Ethereum或者neon-js对于Neo一样。课程内容概述本课程面向广大对EOS开发感兴趣的朋友,是目前市面上理论与实战相结合最全的EOS开发项目,内容涵盖EOS开发相关的基本概念,并围绕EOS钱包项目开发逐步进行讲解,最终实现一个EOS钱包。第一章:概述介绍什么是区块链钱包;分析区块链钱包的应用价值,阐述本课程的目的;并介绍课程项目使用的技术栈,引入对学习者基础知识技能的要求。第二章:理解EOS账户与钱包引入EOS账户、密钥、钱包等概念。介绍如何获取第一个EOS账号,以及如何查询账号信息。解释为什么主网中创建账户的是需要费用的。阐述助记词、keystore、密码与私钥的关系。并通过账户权限与钱包相关的操作,学习EOS账户权限和官方钱包命令等知识。第三章:需求分析与总体设计项目需求分析与总体设计,阐述项目功能模块划分、系统整体架构、前端服务层设计、前端状态机、第三方服务清单等。第四章:前端服务组件实现实现前端服务组件,封装手机钱包的核心功能,例如账号创建、账号导入、转账交易、交易历史查询、资产管理、资源管理等。第五章:前端UI组件实现学习如何利用React实现钱包的前端UI组件,如何利用React-Router前端路由切换组件,如何使用Redux状态库实现前端状态管理。感兴趣的同学可以试试,深入浅出玩转EOS钱包开发,本课程以手机EOS钱包的完整开发过程为主线,深入学习EOS区块链应用开发,课程内容即涵盖账户、计算资源、智能合约、动作与交易等EOS区块链的核心概念,同时也讲解如何使用eosjs和eosjs-ecc开发包访问EOS区块链,以及如何在React前端应用中集成对EOS区块链的支持。课程内容深入浅出,非常适合前端工程师深入学习EOS区块链应用开发。

March 6, 2019 · 1 min · jiezi

基于hashicorp/raft的分布式一致性实战教学

本文由云+社区发表作者:_Super_导语:hashicorp/raft是raft算法的一种比较流行的golang实现,基于它能够比较方便的构建具有强一致性的分布式系统。本文通过实现一个简单的分布式缓存系统来介绍使用hashicorp/raft来构建分布式应用程序的方法。1. 背景 对于后台开发来说,随着业务的发展,由于访问量增大的压力和数据容灾的需要,一定会需要使用分布式的系统,而分布式势必会引入一致性的问题。 一般把一致性分为三种类型:弱一致性、最终一致性、强一致性。这三种模型的一致性强度逐渐递增,实现代价也越来越大。通常弱一致性和最终一致性可以异步冗余,强一致性则是同步冗余,而同步也就意味着影响性能。 对常见的互联网业务来说,使用弱一致性或者最终一致性即可。而使用强一致性一方面会影响系统的性能,另一方面实现也比较困难。常见的一致性协议如zab、raft、paxos,如果由业务纯自己来实现的话代价较大,而且很可能会因为考虑不周而引入其他问题。 对于一些需要强一致性,而又希望花费较小代价的业务来说,使用开源的一致性协议实现组件会是个不错的选择。hashicorp/raft是raft协议的一种golang实现,由hashicorp公司实现并开源,已经在consul等软件中使用。它封装了raft协议的leader选举、log同步等底层实现,基于它能够相对比较容易的构建强一致性的分布式系统,下面以实现一个简单的分布式缓存服务(取名叫stcache)来演示hashicorp/raft的具体使用,完整代码可以在github上下载。2. raft简介 首先还是简单介绍下raft协议。这里不详细介绍raft协议,只是为了方便理解后面的hashicorp/raft的使用步骤而简单列举出raft的一点原理。具体的raft协议可以参考raft的官网,如果已经了解raft协议可以直接跳过这一节。 raft是一种相对易于理解的一致性的协议。它属于leader-follower型的协议,有且只有一个leader,所有的事务请求都由leader处理,leader征求follower的意见,在集群内部达成一致,决定是否执行事务。当leader出现故障,集群中的follower会通过投票的方式选出一个新的leader,维持集群运行。 raft的理论基础是Replicated State Machine,Replicated State Machine需要满足如下的条件:一个server可以有多个state,多个server从同一个start状态出发,都执行相同的command序列,最终到达的stare是一样的。如上图,一般使用replicated log来记录command序列,client的请求被leader转化成log entry,然后通过一致性模块把log同步到各个server,让各个server的log一致。每个server都有state Machine,从start出发,执行完这些log中的command后,server处于相同的state。所以raft协议的关键就是保证各个server的log一致,然后每个server通过执行相同的log来达到一致的状态,理解这点有助于掌握后面对hashicorp/raft的具体使用。3. hashicorp/raft使用3.1 单机版 首先我们创建一个单机版本的stcache,它是一个简单的缓存服务器,在服务内部用一个map来保存数据,只提供简单的get和set操作。type cacheManager struct { data map[string]string sync.RWMutex} 然后stcache开启一个http服务,提供两个api,第一个是set接口,用于设置数据到缓存,成功时返回ok,失败返回错误信息: 第二个是get接口,根据key查询具体的value: 下面我们在单机版stcache的基础上逐步扩充,让它成为一个具有强一致性的分布式系统。3.2 创建节点// NewRaft is used to construct a new Raft node. It takes a configuration, as well// as implementations of various interfaces that are required. If we have any// old state, such as snapshots, logs, peers, etc, all those will be restored// when creating the Raft node.func NewRaft(conf *Config, fsm FSM, logs LogStore, stable StableStore, snaps SnapshotStore, trans Transport) (Raft, error) { hashicorp/raft库提供NewRaft方法来创建一个raft节点,这也是使用这个库的最重要的一个api。NewRaft需要调用层提供6个参数,分别是:Config: 节点配置FSM: finite state machine,有限状态机LogStore: 用来存储raft的日志StableStore: 稳定存储,用来存储raft集群的节点信息等SnapshotStore: 快照存储,用来存储节点的快照信息Transport: raft节点内部的通信通道 下面从这些参数入手看应用程序需要做哪些工作。3.3 Config config是节点的配置信息,我们直接使用raft默认的配置,然后用监听的地址来作为节点的id。config里面还有一些可配置的项,后面我们用到的时候再说。 raftConfig := raft.DefaultConfig() raftConfig.LocalID = raft.ServerID(opts.raftTCPAddress) raftConfig.Logger = log.New(os.Stderr, “raft: “, log.Ldate|log.Ltime)3.4 LogStore 和 StableStore LogStore、StableStore分别用来存储raft log、节点状态信息,hashicorp提供了一个raft-boltdb来实现底层存储,它是一个嵌入式的数据库,能够持久化存储数据,我们直接用它来实现LogStore和StableStore. logStore, err := raftboltdb.NewBoltStore(filepath.Join(opts.dataDir, “raft-log.bolt”)) stableStore, err := raftboltdb.NewBoltStore(filepath.Join(opts.dataDir, “raft-stable.bolt”)) 3.5 SnapshotStore SnapshotStore用来存储快照信息,对于stcache来说,就是存储当前的所有的kv数据,hashicorp内部提供3中快照存储方式,分别是:DiscardSnapshotStore: 不存储,忽略快照,相当于/dev/null,一般用于测试FileSnapshotStore: 文件持久化存储InmemSnapshotStore: 内存存储,不持久化,重启程序会丢失 这里我们使用文件持久化存储。snapshotStore只是提供了一个快照存储的介质,还需要应用程序提供快照生成的方式,后面我们再具体说。 snapshotStore, err := raft.NewFileSnapshotStore(opts.dataDir, 1, os.Stderr)3.6 Transport Transport是raft集群内部节点之间的通信渠道,节点之间需要通过这个通道来进行日志同步、leader选举等。hashicorp/raft内部提供了两种方式来实现,一种是通过TCPTransport,基于tcp,可以跨机器跨网络通信;另一种是InmemTransport,不走网络,在内存里面通过channel来通信。显然一般情况下都使用TCPTransport即可,在stcache里也采用tcp的方式。func newRaftTransport(opts options) (raft.NetworkTransport, error) { address, err := net.ResolveTCPAddr(“tcp”, opts.raftTCPAddress) if err != nil { return nil, err } transport, err := raft.NewTCPTransport(address.String(), address, 3, 10time.Second, os.Stderr) if err != nil { return nil, err } return transport, nil}3.7 FSM 最后再看FSM,它是一个interface,需要应用程序来实现3个funcition。/FSM provides an interface that can be implemented byclients to make use of the replicated log./type FSM interface { / Apply log is invoked once a log entry is committed. It returns a value which will be made available in the ApplyFuture returned by Raft.Apply method if that method was called on the same Raft node as the FSM./ Apply(*Log) interface{} // Snapshot is used to support log compaction. This call should // return an FSMSnapshot which can be used to save a point-in-time // snapshot of the FSM. Apply and Snapshot are not called in multiple // threads, but Apply will be called concurrently with Persist. This means // the FSM should be implemented in a fashion that allows for concurrent // updates while a snapshot is happening. Snapshot() (FSMSnapshot, error) // Restore is used to restore an FSM from a snapshot. It is not called // concurrently with any other command. The FSM must discard all previous // state. Restore(io.ReadCloser) error} 第一个是Apply,当raft内部commit了一个log entry后,会记录在上面说过的logStore里面,被commit的log entry需要被执行,就stcache来说,执行log entry就是把数据写入缓存,即执行set操作。我们改造doSet方法, 这里不再直接写缓存,而是调用raft的Apply方式,为这次set操作生成一个log entry,这里面会根据raft的内部协议,在各个节点之间进行通信协作,确保最后这条log 会在整个集群的节点里面提交或者失败。// doSet saves data to cache, only raft master node provides this apifunc (h *httpServer) doSet(w http.ResponseWriter, r http.Request) { // … get params from request url event := logEntryData{Key: key, Value: value} eventBytes, err := json.Marshal(event) if err != nil { h.log.Printf(“json.Marshal failed, err:%v”, err) fmt.Fprint(w, “internal error\n”) return } applyFuture := h.ctx.st.raft.raft.Apply(eventBytes, 5time.Second) if err := applyFuture.Error(); err != nil { h.log.Printf(“raft.Apply failed:%v”, err) fmt.Fprint(w, “internal error\n”) return } fmt.Fprintf(w, “ok\n”)} 对follower节点来说,leader会通知它来commit log entry,被commit的log entry需要调用应用层提供的Apply方法来执行日志,这里就是从logEntry拿到具体的数据,然后写入缓存里面即可。// Apply applies a Raft log entry to the key-value store.func (f *FSM) Apply(logEntry *raft.Log) interface{} { e := logEntryData{} if err := json.Unmarshal(logEntry.Data, &e); err != nil { panic(“Failed unmarshaling Raft log entry.”) } ret := f.ctx.st.cm.Set(e.Key, e.Value) return ret} 3.7.1 snapshot FSM需要提供的另外两个方法是Snapshot()和Restore(),分别用于生成一个快照结构和根据快照恢复数据。首先我们需要定义快照,hashicorp/raft内部定义了快照的interface,需要实现两个func,Persist用来生成快照数据,一般只需要实现它即可;Release则是快照处理完成后的回调,不需要的话可以实现为空函数。// FSMSnapshot is returned by an FSM in response to a Snapshot// It must be safe to invoke FSMSnapshot methods with concurrent// calls to Apply.type FSMSnapshot interface { // Persist should dump all necessary state to the WriteCloser ‘sink’, // and call sink.Close() when finished or call sink.Cancel() on error. Persist(sink SnapshotSink) error // Release is invoked when we are finished with the snapshot. Release()} 我们定义一个简单的snapshot结构,在Persist里面,自己把缓存里面的数据用json格式化的方式来生成快照,sink.Write就是把快照写入snapStore,我们刚才定义的是FileSnapshotStore,所以会把数据写入文件。type snapshot struct { cm *cacheManager}// Persist saves the FSM snapshot out to the given sink.func (s *snapshot) Persist(sink raft.SnapshotSink) error { snapshotBytes, err := s.cm.Marshal() if err != nil { sink.Cancel() return err } if _, err := sink.Write(snapshotBytes); err != nil { sink.Cancel() return err } if err := sink.Close(); err != nil { sink.Cancel() return err } return nil}func (f *snapshot) Release() {}3.7.2 snapshot保存与恢复 而快照生成和保存的触发条件除了应用程序主动触发外,还可以在Config里面设置SnapshotInterval和SnapshotThreshold,前者指每间隔多久生成一次快照,后者指每commit多少log entry后生成一次快照。需要两个条件同时满足才会生成和保存一次快照,默认config里面配置的条件比较高,我们可以自己修改配置,比如在stcache里面配置SnapshotInterval为20s,SnapshotThreshold为2,表示当满足距离上次快照保存超过20s,且log增加2条的时候,保存一个新的快照。 raftConfig := raft.DefaultConfig() raftConfig.LocalID = raft.ServerID(opts.raftTCPAddress) raftConfig.Logger = log.New(os.Stderr, “raft: “, log.Ldate|log.Ltime) raftConfig.SnapshotInterval = 20 * time.Second raftConfig.SnapshotThreshold = 2 服务重启的时候,会先读取本地的快照来恢复数据,在FSM里面定义的Restore函数会被调用,这里我们就简单的对数据解析json反序列化然后写入内存即可。至此,我们已经能够正常的保存快照,也能在重启的时候从文件恢复快照数据。// Restore stores the key-value store to a previous state.func (f *FSM) Restore(serialized io.ReadCloser) error { return f.ctx.st.cm.UnMarshal(serialized)}// UnMarshal deserializes cache datafunc (c *cacheManager) UnMarshal(serialized io.ReadCloser) error { var newData map[string]string if err := json.NewDecoder(serialized).Decode(&newData); err != nil { return err } c.Lock() defer c.Unlock() c.data = newData return nil}3.8 集群建立 集群最开始的时候只有一个节点,我们让第一个节点通过bootstrap的方式启动,它启动后成为leader。 if opts.bootstrap { configuration := raft.Configuration{ Servers: []raft.Server{ { ID: raftConfig.LocalID, Address: transport.LocalAddr(), }, }, } raftNode.BootstrapCluster(configuration) } 后续的节点启动的时候需要加入集群,启动的时候指定第一个节点的地址,并发送请求加入集群,这里我们定义成直接通过http请求。// joinRaftCluster joins a node to raft clusterfunc joinRaftCluster(opts *options) error { url := fmt.Sprintf(“http://%s/join?peerAddress=%s”, opts.joinAddress, opts.raftTCPAddress) resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } if string(body) != “ok” { return errors.New(fmt.Sprintf(“Error joining cluster: %s”, body)) } return nil} 先启动的节点收到请求后,获取对方的地址(指raft集群内部通信的tcp地址),然后调用AddVoter把这个节点加入到集群即可。申请加入的节点会进入follower状态,这以后集群节点之间就可以正常通信,leader也会把数据同步给follower。// doJoin handles joining cluster requestfunc (h *httpServer) doJoin(w http.ResponseWriter, r *http.Request) { vars := r.URL.Query() peerAddress := vars.Get(“peerAddress”) if peerAddress == "” { h.log.Println(“invalid PeerAddress”) fmt.Fprint(w, “invalid peerAddress\n”) return } addPeerFuture := h.ctx.st.raft.raft.AddVoter(raft.ServerID(peerAddress), raft.ServerAddress(peerAddress), 0, 0) if err := addPeerFuture.Error(); err != nil { h.log.Printf(“Error joining peer to raft, peeraddress:%s, err:%v, code:%d”, peerAddress, err, http.StatusInternalServerError) fmt.Fprint(w, “internal error\n”) return } fmt.Fprint(w, “ok”)}3.9 故障切换 当集群的leader故障后,集群的其他节点能够感知到,并申请成为leader,在各个follower中进行投票,最后选取出一个新的leader。leader选举是属于raft协议的内容,不需要应用程序操心,但是对有些场景而言,应用程序需要感知leader状态,比如对stcache而言,理论上只有leader才能处理set请求来写数据,follower应该只能处理get请求查询数据。为了模拟说明这个情况,我们在stcache里面我们设置一个写标志位,当本节点是leader的时候标识位置true,可以处理set请求,否则标识位为false,不能处理set请求。// doSet saves data to cache, only raft master node provides this apifunc (h *httpServer) doSet(w http.ResponseWriter, r *http.Request) { if !h.checkWritePermission() { fmt.Fprint(w, “write method not allowed\n”) return } // … set data } 当故障切换的时候,follower变成了leader,应用程序如何感知到呢? 在raft结构里面提供有一个eaderCh,它是bool类型的channel,不带缓存,当本节点的leader状态有变化的时候,会往这个channel里面写数据,但是由于不带缓冲且写数据的协程不会阻塞在这里,有可能会写入失败,没有及时变更状态,所以使用leaderCh的可靠性不能保证。好在raft Config里面提供了另一个channel NotifyCh,它是带缓存的,当leader状态变化时会往这个chan写数据,写入的变更消息能够缓存在channel里面,应用程序能够通过它获取到最新的状态变化。 我们首先在初始化config时候创建一个带缓存的chan,把它赋值给config里面的NotifyCh,然后在节点启动后监听这个chan,当本节点的leader状态变化时(变成leader或者从leader变成follower),就能够从这个chan里面读取到bool值,并调整我们先前设置的写标志位,控制是否能否处理set操作。func newRaftNode(opts *options, ctx *stCachedContext) (*raftNodeInfo, error) { raftConfig := raft.DefaultConfig() raftConfig.LocalID = raft.ServerID(opts.raftTCPAddress) raftConfig.Logger = log.New(os.Stderr, “raft: “, log.Ldate|log.Ltime) raftConfig.SnapshotInterval = 20 * time.Second raftConfig.SnapshotThreshold = 2 leaderNotifyCh := make(chan bool, 1) raftConfig.NotifyCh = leaderNotifyCh // … }4. 成果演示 做完上面的工作后,我们来测试下效果,我们同一台机器上启动3个节点来构成一个集群,第一个节点用bootstrapt的方式启动,成为leader 第二个节点和第三个节点启动时指定加入集群,成为follower 现在集群中有3个节点,leader监听127.0.01:6000对外提供set和get接口,两个follower分别监听127.0.0.1:6001和127.0.0.1:6002,对外提供get接口。4.1 集群数据同步 通过调用leader的set接口写入一个数据,key是ping,value是pong 这时候能在两个follower上看见apply的日志,follower节点写入了log,并收到leader的通知提交数据。 通过查询接口,也能从follower里面查询到刚才写入的数据,证明数据同步没有问题。 有一点需要说明的事,我们这里从follower是可能读不到最新数据的。由于leader对set操作返回的时候,follower可能还没有apply数据,所以从follower的get查询可能返回旧数据或者空数据。如果要保证能从follower查询到的一定是最新的数据还需要很多额外的工作,即做到linearizable read,有兴趣可以看这篇测试文章,这里不再展开。4.2 快照保存与恢复 我们再通过set接口写入两个数据,能看见节点开始保存快照 在指定的目录下面,能看见快照的具体信息,有两个文件,meta.json保存了版本号、log序号、集群节点地址等集群信息;state.bin里面是快照数据,这里就是我们刚刚写入的数据被json序列化后的字符串。 现在把节点都停止,然后重新启动leader,内存的数据都丢失,它会从保存的快照文件里面恢复数据。重启follower也一样会从自己保存的快照里面加载数据。4.3 leader切换 把leader和follower都重启恢复,现在leader监听127.0.01:6000,只有它能执行set操作,follower只能执行get操作 我们停掉leader节点,两个follower会开始选举,这里node2赢得了选举,进入leader状态,并且它开始打开set操作 我们再请求node2监听的127.0.0.1:6001,发现已经可以正常写入数据了,leader切换顺利完成。 我们再重启原来的leader节点,它会变成follower,并从新的leader(也就是node2)这里同步它所缺失的数据。5. 总结 上面所创建的stcache只是一个简单的示例程序,真正要做到在线上使用还有很多问题需要考虑,目前基于hashicorp/raft比较成熟的开源软件有consul,如果有兴趣可以通过它做进一步研究。 总的来说,hashicorp/raft封装了raft的内部协议,提供简洁明了的使用方法,基于它能够很快速地构建出具有强一致性的应用程序。此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

March 1, 2019 · 5 min · jiezi

f-admin——基于Laravel框架开发的基础权限后台系统

f-admin基础权限后台❤️ 本项目 GitHub / Gitee(码云),目前已在公司产品应用,运行在数个客户服务器内。f-admin基础权限后台是一套基于Laravel框架开发的系统,不需要开发者重复不必要的工作,就可以实现后台功能的快速开发,其主要特点包括:[x] 集成 Composer,安装使用方便。[x] 用户管理可以配置自己的权限。[x] 角色管理可以配置用户及权限。[x] 权限控制可以精确到某一个请求的控制。[x] 菜单可以设置自己的图标,可以控制哪些角色可以看到。[x] 日志查看搜索。[x] 严格的前端后端输入验证。[x] pc端和手机端都能适配。[ ] 其它优化,持续进行中 ……f-admin的运行环境要求PHP5.4以上;laravel框架要求为5.4。线上DEMO f-admin 你也可以用手机扫下二维码查看手机效果 导航效果预览- 首页- 用户管理- 角色管理- 权限管理- 菜单管理- 日志管理安装步骤- 1.获取代码- 2.安装依赖- 3.生成APP_KEY- 4.修改env配置- 5.数据库迁移- 6.访问首页环境配置- 1.windows- 2.linux(apache)- 3.linux(nginx)感谢效果预览(pc/mobile)首页用户管理角色管理权限管理菜单管理日志管理安装步骤1.获取代码新建一个文件夹,进入该文件夹,利用git等工具输入以下命令:git init git clone https://github.com/fangzesheng/f-admin.git2.安装依赖composer install 3.生成APP_KEYcp .env.example .envphp artisan key:generate 4.修改 .env 配置DB_CONNECTION=mysqlDB_HOST=your_hostDB_PORT=your_portDB_DATABASE=your_dbDB_USERNAME=your_usernameDB_PASSWORD=your_pwdCACHE_DRIVER=array //将file改为array5.数据库迁移php artisan migratecomposer dump-autoloadphp artisan db:seed如果在执行php artisan migrate增加表操作出现字段长度过长错误时,则可能是因为mysql版本低于5.5.3,解决方法:a.升级mysqlb.手动配置迁移命令migrate生成的默认字符串长度,在appProvidersAppServiceProvider中调用一下方法来实现配置记得先将新建数据库里面的表清空!!!use Illuminate\Support\Facades\Schema; public function boot(){ Schema::defaultStringLength(191);}6.访问首页访问自己的配置好的域名 用户名:admin 密码:f123456环境配置(仅供参考)1.windows<VirtualHost *:80> DocumentRoot E:\test\public ServerName www.test.com <Directory “E:\test\public”> AllowOverride All order deny,allow Require all granted </Directory></VirtualHost>2.linux(apache)<VirtualHost *:80> DocumentRoot /data/wwwroot/default/f-admin/public ServerName www.fang99.cc <Directory “/data/wwwroot/default/f-admin/public”> AllowOverride All order deny,allow Require all granted </Directory></VirtualHost>3.linux(nginx)server { listen 8088; server_name demo.fang99.cc; location / { index index.php index.html; root /var/www/f-admin/public/; try_files $uri $uri/ /index.php?$query_string; } location ~ .php$ { root /var/www/f-admin/public/; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_intercept_errors on; include /etc/nginx/fastcgi.conf; }}感谢layerlaravel如果你觉得这个开源项目对你有用,欢迎star你懂的!谢谢:) ...

February 26, 2019 · 1 min · jiezi

美团点评技术年货:一本覆盖各技术领域、1200+页的电子书

春节已近,年味渐浓。又到了我们献上技术年货的时候。不久前,我们已经给大家分享了技术沙龙大套餐,汇集了过去一年我们线上线下技术沙龙99位讲师,85个演讲,70+小时分享。今天出场的,同样重磅——技术博客2018年大合集。2018年,是美团技术团队官方博客第5个年头,博客网站全年独立访问用户累计超过300万,微信公众号(meituantech)的关注数也超过了15万。由衷地感谢大家一直以来对我们的鼓励和陪伴!在2019年春节到来之际,我们再次精选了114篇技术干货,制作成一本厚达1200多页的电子书呈送给大家。这本电子书主要包括前端、后台、系统、算法、测试、运维、工程师成长等7个板块。疑义相与析,大家在阅读中如果发现Bug、问题,欢迎扫描文末二维码,通过微信公众号与我们交流。也欢迎大家转给有相同兴趣的同事、朋友,一起切磋,共同成长。最后祝大家,新春快乐,阖家幸福。如何获取?长按并识别下方的二维码,关注“美团技术团队”官方公众号,回复 “年货”,即可免费在线阅读、下载2018美团点评技术文章精选。温馨提示:我们提供了电子书的下载链接,大家可以选择性下载。2018美团点评技术年货合辑:1200+页,约350M;前端系列:共566页,约90M;后台系列:共229页,约60M;系统系列:共165页,约48M;算法系列:共183页,约56M;运维系列:共140页,约36M;测试系列:共88页,约28M;工程师成长系列:共49页,约16M。备注:因文件较大,缓存需要一定的时间,需要您的一点耐心,请注意自己的流量。Android系统用户建议通过WiFi环境下载。iOS系统用户建议将地址复制到PC端,使用浏览器进行下载。

January 28, 2019 · 1 min · jiezi

Spring Boot引起的“堆外内存泄漏”排查及经验总结

背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于Spring Boot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m, -Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M”,实际使用的物理内存如下图所示:排查过程1. 使用Java层面的工具定位内存区域(堆内内存、Code区域或者使用unsafe.allocateMemory和DirectByteBuffer申请的堆外内存)笔者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,使用命令jcmd pid VM.native_memory detail查看到的内存分布如下:发现命令显示的committed的内存小于物理内存,因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。为了防止误判,笔者使用了pmap查看内存分布,发现大量的64M的地址;而这些地址空间不在jcmd命令所给出的地址空间里面,基本上就断定就是这些64M的内存所导致。2. 使用系统层面的工具定位堆外内存因为笔者已经基本上确定是Native Code所引起,而Java层面的工具不便于排查此类问题,只能使用系统层面的工具去定位问题。首先,使用了gperftools去定位问题gperftools的使用方法可以参考gperftools,gperftools的监控如下:从上图可以看出:使用malloc申请的的内存最高到3G之后就释放了,之后始终维持在700M-800M。笔者第一反应是:难道Native Code中没有使用malloc申请,直接使用mmap/brk申请的?(gperftools原理就使用动态链接的方式替换了操作系统默认的内存分配器(glibc)。)然后,使用strace去追踪系统调用因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪向OS申请内存请求,但是并没有发现有可疑内存申请。strace监控如下图所示:接着,使用GDB去dump可疑内存因为使用strace没有追踪到可疑内存申请;于是想着看看内存中的情况。就是直接使用命令gdp -pid pid进入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可以从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。再次,项目启动时使用strace去追踪系统调用项目启动使用strace追踪系统调用,发现确实申请了很多64M的内存空间,截图如下:使用该mmap申请的地址空间在pmap对应如下:最后,使用jstack去查看对应的线程因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意10进制和16进制转换)如下:这里基本上就可以看出问题来了:MCC(美团统一配置中心)使用了Reflections进行扫包,底层使用了Spring Boot去加载JAR。因为解压JAR使用Inflater类,需要用到堆外内存,然后使用Btrace去追踪这个类,栈如下:然后查看使用MCC的地方,发现没有配置扫包路径,默认是扫描所有的包。于是修改代码,配置扫包路径,发布上线后内存问题解决。3. 为什么堆外内存没有释放掉呢?虽然问题已经解决了,但是有几个疑问:为什么使用旧的框架没有问题?为什么堆外内存没有释放?为什么内存大小都是64M,JAR大小不可能这么大,而且都是一样大?为什么gperftools最终显示使用的的内存大小是700M左右,解压包真的没有使用malloc申请内存吗?带着疑问,笔者直接看了一下Spring Boot Loader那一块的源码。发现Spring Boot对Java JDK的InflaterInputStream进行了包装并且使用了Inflater,而Inflater本身用于解压JAR包的需要用到堆外内存。而包装之后的类ZipInflaterInputStream没有释放Inflater持有的堆外内存。于是笔者以为找到了原因,立马向Spring Boot社区反馈了这个bug。但是反馈之后,笔者就发现Inflater这个对象本身实现了finalize方法,在这个方法中有调用释放堆外内存的逻辑。也就是说Spring Boot依赖于GC释放堆外内存。笔者使用jmap查看堆内对象时,发现已经基本上没有Inflater这个对象了。于是就怀疑GC的时候,没有调用finalize。带着这样的怀疑,笔者把Inflater进行包装在Spring Boot Loader里面替换成自己包装的Inflater,在finalize进行打点监控,结果finalize方法确实被调用了。于是笔者又去看了Inflater对应的C代码,发现初始化的使用了malloc申请内存,end的时候也调用了free去释放内存。此刻,笔者只能怀疑free的时候没有真正释放内存,便把Spring Boot包装的InflaterInputStream替换成Java JDK自带的,发现替换之后,内存问题也得以解决了。这时,再返过来看gperftools的内存分布情况,发现使用Spring Boot时,内存使用一直在增加,突然某个点内存使用下降了好多(使用量直接由3G降为700M左右)。这个点应该就是GC引起的,内存应该释放了,但是在操作系统层面并没有看到内存变化,那是不是没有释放到操作系统,被内存分配器持有了呢?继续探究,发现系统默认的内存分配器(glibc 2.12版本)和使用gperftools内存地址分布差别很明显,2.5G地址使用smaps发现它是属于Native Stack。内存地址分布如下:到此,基本上可以确定是内存分配器在捣鬼;搜索了一下glibc 64M,发现glibc从2.11开始对每个线程引入内存池(64位机器大小就是64M内存),原文如下:按照文中所说去修改MALLOC_ARENA_MAX环境变量,发现没什么效果。查看tcmalloc(gperftools使用的内存分配器)也使用了内存池方式。为了验证是内存池搞的鬼,笔者就简单写个不带内存池的内存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成动态库,然后使用export LD_PRELOAD=zjbmalloc.so替换掉glibc的内存分配器。其中代码Demo如下:#include<sys/mman.h>#include<stdlib.h>#include<string.h>#include<stdio.h>//作者使用的64位机器,sizeof(size_t)也就是sizeof(long) void* malloc ( size_t size ){ long* ptr = mmap( 0, size + sizeof(long), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0 ); if (ptr == MAP_FAILED) { return NULL; } ptr = size; // First 8 bytes contain length. return (void)(&ptr[1]); // Memory that is after length variable}void calloc(size_t n, size_t size) { void ptr = malloc(n * size); if (ptr == NULL) { return NULL; } memset(ptr, 0, n * size); return ptr;}void *realloc(void ptr, size_t size){ if (size == 0) { free(ptr); return NULL; } if (ptr == NULL) { return malloc(size); } long plen = (long)ptr; plen–; // Reach top of memory long len = plen; if (size <= len) { return ptr; } void rptr = malloc(size); if (rptr == NULL) { free(ptr); return NULL; } rptr = memcpy(rptr, ptr, len); free(ptr); return rptr;}void free (void ptr ){ if (ptr == NULL) { return; } long plen = (long)ptr; plen–; // Reach top of memory long len = plen; // Read length munmap((void)plen, len + sizeof(long));}通过在自定义分配器当中埋点可以发现其实程序启动之后应用实际申请的堆外内存始终在700M-800M之间,gperftools监控显示内存使用量也是在700M-800M左右。但是从操作系统角度来看进程占用的内存差别很大(这里只是监控堆外内存)。笔者做了一下测试,使用不同分配器进行不同程度的扫包,占用的内存如下:为什么自定义的malloc申请800M,最终占用的物理内存在1.7G呢?因为自定义内存分配器采用的是mmap分配内存,mmap分配内存按需向上取整到整数个页,所以存在着巨大的空间浪费。通过监控发现最终申请的页面数目在536k个左右,那实际上向系统申请的内存等于512k * 4k(pagesize) = 2G。为什么这个数据大于1.7G呢?因为操作系统采取的是延迟分配的方式,通过mmap向系统申请内存的时候,系统仅仅返回内存地址并没有分配真实的物理内存。只有在真正使用的时候,系统产生一个缺页中断,然后再分配实际的物理Page。总结整个内存分配的流程如上图所示。MCC扫包的默认配置是扫描所有的JAR包。在扫描包的时候,Spring Boot不会主动去释放堆外内存,导致在扫描阶段,堆外内存占用量一直持续飙升。当发生GC的时候,Spring Boot依赖于finalize机制去释放了堆外内存;但是glibc为了性能考虑,并没有真正把内存归返到操作系统,而是留下来放入内存池了,导致应用层以为发生了“内存泄漏”。所以修改MCC的配置路径为特定的JAR包,问题解决。笔者在发表这篇文章时,发现Spring Boot的最新版本(2.0.5.RELEASE)已经做了修改,在ZipInflaterInputStream主动释放了堆外内存不再依赖GC;所以Spring Boot升级到最新版本,这个问题也可以得到解决。参考资料GNU C Library (glibc)Native Memory TrackingSpring BootgperftoolsBtrace作者简介纪兵,2015年加入美团,目前主要从事酒店C端相关的工作。 ...

January 4, 2019 · 2 min · jiezi

被忽略的后台开发神器 — Docker

刚接触Docker的时候,以为只是用来做运维。后来真正用的时候才发觉,这个Docker简直是个神器。不管什么开发场景都能轻松应付。想要什么环境都能随意生成,而且灵活性更高,更轻量,完美实现微服务的概念。什么是DockerDocker是一个开源的应用容器引擎,基于Go语言 并遵从Apache2.0协议开源。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。它占用的资源更少,能做到的事更多。与传统虚拟机的对比特性容器虚拟机启动秒级分钟级 硬盘启动一般为MB一般为GB性能接近原生弱于系统支持量单机支持上千个容器一般几十个安装Docker安装的方法都挺简单的,我用的是mac,直接通过Docker官网下载软件安装,全程无障碍。Docker概念镜像(images):Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。(直白点可以理解为系统安装包)容器(container):镜像和容器的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。(可以理解为安装好的系统)Docker镜像使用一、下载镜像大概了解了Docker的概念以后,我们就尝试拉取flask镜像使用一下。查找镜像可以通过https://hub.docker.com/网站来搜索,或者通过命令搜索。docker search flask在这里,我是通过Docker hub官网挑选出了python3.7 + alpine3.8组合的运行环境,alpine是精简版的linux,体积更小、运行的资源消耗更少。# 拉取镜像docker pull tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8# 下载好可查看镜像列表是否存在docker images二、运行flask镜像下载镜像以后,就开始运行下试试,感受一下Docker的轻量、快捷。首先创建个flask运行文件来,在这里,我创建了/docker/flask作为项目文件,然后在根目录下再创建个app文件夹来存放main.py文件,代码如下:from flask import Flaskapp = Flask(name)@app.route("/")def hello(): return “Hello World from Flask!“if name == “main”: # 测试环境下才开启debug模式 app.run(host=‘0.0.0.0’, debug=True, port=80)现在的文件结构:flask └── app └── main.py运行命令docker run -it –name test -p 8080:80 -v /docker/flask/app:/app -w /app tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8 python main.py这里说明一下命令的参数含义:-it 是将-i -t合并起来,作用是可以用指定终端对容器执行命令交互。–name 对容器进行命名。-p 将主机的8080端口映射到容器的80端口。-v 将主机的/docker/flask/app文件挂载到容器的/app文件,如果容器内没有的话会自动创建。-w 将/app文件作为工作区,后面的执行命令都默认在该文件路径下执行。tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8 镜像名跟标签。python main.py 通过python来运行工作区的main.py文件。运行结果:现在主机跟容器的链接已经建立起来了,主机通过8080端口就能访问到容器的网站。自定义镜像在使用别人定制的镜像时总是不能尽善尽美的,如果在自己项目里面,不能每次都是拉取下来重新配置一下。像上面的镜像,我可不喜欢这么长的名字,想想每次要敲这么长的名字都头疼(tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8)。编写Dockerfile文件打开我们刚才的/docker/flask路径,在根目录下创建Dockerfile文件,内容如下。# 基础镜像FROM tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8# 没有vim来查看文件很不习惯,利用alpine的包管理安装一个来RUN apk add vim# 顺便用pip安装个redis包,后面用得上RUN pip3 install redis# 将我们的app文件加入到自定义镜像里面去COPY ./app /app现在我们的文件结构是:flask├── app│ └── main.py└── Dockerfile剩下的就跑一遍就OK啦!记得一定要在Dockerfile文件同级目录下执行build命令。docker build -t myflask .Sending build context to Docker daemon 4.608kBStep 1/4 : FROM tiangolo/uwsgi-nginx-flask:python3.7-alpine3.8 —> c69984ff0683Step 2/4 : RUN apk add vim —> Using cache —> ebe2947fcf89Step 3/4 : RUN pip3 install redis —> Running in aa774ba9030eCollecting redis Downloading https://files.pythonhosted.org/packages/f5/00/5253aff5e747faf10d8ceb35fb5569b848cde2fdc13685d42fcf63118bbc/redis-3.0.1-py2.py3-none-any.whl (61kB)Installing collected packages: redisSuccessfully installed redis-3.0.1Removing intermediate container aa774ba9030e —> 47a0f1ce8ea2Step 4/4 : COPY ./app /app —> 50908f081641Successfully built 50908f081641Successfully tagged myflask:latest-t 指定要创建的目标路径。. 这里有个点记住啦,表示是当前路径下的Dockerfile文件,可以指定为绝对路径。编译完后就通过docker images查看一下,就能看到myflask镜像了,里面能直接运行python main.py来启动flask,并且内置了vim和redis包。Docker Compose让多容器成为一个整体我们的每个容器都负责一个服务,这样容器多的时候一个个手动启动的话是不现实的。在这种情况我们可以通过Docker Compose来关联每个容器,组成一个完整的项目。Compose项目由Python编写,实现上调用了 Docker服务提供的 API 来对容器进行管理。# 安装docker-composesudo pip3 install docker-compose实现能记录访问次数的web在这里,我们通过docker-compose.yml文件来启动flask容器和redis容器,并将两个不同容器相互关联起来。首先在/docker/flask目录下创建docker-compose.yml文件,内容如下:version: ‘3’services: flask: image: myflask container_name: myflask ports: - 8080:80 volumes: - /docker/flask/app:/app working_dir: /app # 运行后执行的命令 command: python main.py redis: # 如果没有这个镜像的话会自动下载 image: “redis:latest” container_name: myredis然后我们把上面的main.py代码修改一下,连接redis数据库并记录网站访问次数。main.py修改后内容如下:from flask import Flaskfrom redis import Redisapp = Flask(name)redis = Redis(host=‘redis’, port=6379)@app.route(”/")def hello(): count = redis.incr(‘visit’) return f"Hello World from Flask! 该页面已被访问{count}次。“if name == “main”: # Only for debugging while developing app.run(host=‘0.0.0.0’, debug=True, port=80)目前的文件结构是:flask├── app│ └── main.py└── Dockerfile└── docker-compose.yml这些编排的文件参数都是取自于Docker,基本都能看懂,其它就没啥啦,直接命令行跑起来:docker-compose up就辣么简单!现在我们在浏览器上访问http://localhost:8080/就能看到结果了,并且每访问一次这页面都会自动增加访问次数.在这里,我们也能通过docker ps命令查看运行中的容器:docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES66133318452d redis:latest “docker-entrypoint.s…” 13 seconds ago Up 12 seconds 6379/tcp myredis0956529c3c9c myflask “/entrypoint.sh pyth…” 13 seconds ago Up 11 seconds 443/tcp, 0.0.0.0:8080->80/tcp myflask有了Docker Compose的Docker才是完整的Docker,有了这些以后开发简直不要太爽,每个容器只要维护自己的服务环境就ok了。Docker的日常操作镜像常用操作# 下载镜像docker pull name# 列出本地镜像docker images# 使用镜像运行生成容器docker run name:tag# 删除镜像docker rmi id/name容器常用操作可以通过容器的id或者容器别名来启动、停止、重启。# 查看运行中的容器docker ps# 查看所有生成的容器docker ps -a# 开始容器docker start container# 停止容器docker stop container# 重启容器docker restart container# 移除不需要的容器(移除前容器必须要处于停止状态)docker rm container# 进入后台运行的容器docker exec -it container /bin/sh# 打印容器内部的信息(-f参数能实时观察内部信息)docker logs -f container通过-i -t进来容器的,可以先按ctrl + p, 然后按ctrl + q来退出交互界面组,这样退出不会关闭容器。docker-compose常用操作# 自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。docker-compose up# 此命令将会停止 up 命令所启动的容器,并移除网络docker-compose down# 启动已经存在的服务容器。docker-compose start# 停止已经处于运行状态的容器,但不删除它。通过start可以再次启动这些容器。docker-compose stop# 重启项目中的服务docker-compose restart默认情况,docker-compose up启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。当通过Ctrl-C停止命令时,所有容器将会停止。结语这次接触Docker的时间虽然不长,但是这种微服务细分的架构真的是惊艳到我了。以前玩过VM虚拟机,那个使用成本太高,不够灵活,用过一段时间就放弃了,老老实实维护自己的本机环境。有了这个Docker以后,想要什么测试环境都行,直接几行代码生成就好,一种随心所欲的自由。上面写的那些都是日常使用的命令,能应付基本的需求了,真要深入的话建议去找详细的文档,我就不写太累赘了,希望大家都能去接触一下这个Docker,怎么都不亏,你们也会喜欢上这小鲸鱼的。 ...

December 27, 2018 · 2 min · jiezi

Netty堆外内存泄露排查与总结

导读Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 JDK 的 NIO 或者其他NIO框架:使用 JDK 自带的 NIO 需要了解太多的概念,编程复杂。Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动。Netty自带的拆包解包,异常检测等机制让我们从 NIO 的繁重细节中脱离出来,只需关心业务逻辑即可。Netty解决了JDK 的很多包括空轮训在内的 Bug。Netty底层对线程,Selector 做了很多细小的优化,精心设计的 Reactor 线程做到非常高效的并发处理。自带各种协议栈,让我们处理任何一种通用协议都几乎不用亲自动手。Netty社区活跃,遇到问题随时邮件列表或者 issue。Netty已经历各大RPC框架(Dubbo),消息中间件(RocketMQ),大数据通信(Hadoop)框架的广泛的线上验证,健壮性无比强大。背景最近在做一个基于 Websocket 的长连中间件,服务端使用实现了 Socket.IO 协议(基于WebSocket协议,提供长轮询降级能力) 的 netty-socketio 框架,该框架为 Netty 实现,鉴于本人对 Netty 比较熟,并且对比同样实现了 Socket.IO 协议的其他框架,Netty 的口碑都要更好一些,因此选择这个框架作为底层核心。诚然,任何开源框架都避免不了 Bug 的存在,我们在使用这个开源框架时,就遇到一个堆外内存泄露的 Bug。美团的价值观一直都是“追求卓越”,所以我们就想挑战一下,找到那只臭虫(Bug),而本文就是遇到的问题以及排查的过程。当然,想看结论的同学可以直接跳到最后,阅读总结即可。问题某天早上,我们突然收到告警,Nginx 服务端出现大量5xx。我们使用 Nginx 作为服务端 WebSocket 的七层负载,5xx的爆发通常表明服务端不可用。由于目前 Nginx 告警没有细分具体哪台机器不可用,接下来,我们就到 CAT(美团点评统一监控平台,目前已经开源)去检查一下整个集群的各项指标,就发现如下两个异常:某台机器在同一时间点爆发 GC(垃圾回收),而且在同一时间,JVM 线程阻塞。接下来,我们就就开始了漫长的堆外内存泄露“排查之旅”。排查过程阶段1: 怀疑是log4j2因为线程被大量阻塞,我们首先想到的是定位哪些线程被阻塞,最后查出来是 Log4j2 狂打日志导致 Netty 的 NIO 线程阻塞(由于没有及时保留现场,所以截图缺失)。NIO 线程阻塞之后,因我们的服务器无法处理客户端的请求,所以对Nginx来说就是5xx。接下来,我们查看了 Log4j2 的配置文件。我们发现打印到控制台的这个 appender 忘记注释掉了,所以初步猜测:因为这个项目打印的日志过多,而 Log4j2 打印到控制台是同步阻塞打印的,所以就导致了这个问题。那么接下来,我们把线上所有机器的这行注释掉,本以为会“大功告成”,但没想到仅仅过了几天,5xx告警又来“敲门”。看来,这个问题并没我们最初想象的那么简单。阶段2:可疑日志浮现接下来,我们只能硬着头皮去查日志,特别是故障发生点前后的日志,于是又发现了一处可疑的地方:可以看到:在极短的时间内,狂打 failed to allocate 64(bytes) of direct memory(…)日志(瞬间十几个日志文件,每个日志文件几百M),日志里抛出一个 Netty 自己封装的 OutOfDirectMemoryError。说白了,就是堆外内存不够用,Netty 一直在“喊冤”。堆外内存泄露,听到这个名词就感到很沮丧。因为这个问题的排查就像 C 语言内存泄露一样难以排查,首先能想到的就是,在 OOM 爆发之前,查看有无异常。然后查遍了 CAT 上与机器相关的所有指标,查遍了 OOM 日志之前的所有日志,均未发现任何异常!这个时候心里已经“万马奔腾”了……阶段3:定位OOM源没办法,只能看着这堆讨厌的 OOM 日志发着呆,希望答案能够“蹦到”眼前,但是那只是妄想。一筹莫展之际,突然一道光在眼前一闪而过,在 OOM 下方的几行日志变得耀眼起来(为啥之前就没想认真查看日志?估计是被堆外内存泄露这几个词吓怕了吧 ==!),这几行字是 ….PlatformDepedeng.incrementMemory()…。原来,堆外内存是否够用,是 Netty 这边自己统计的,那么是不是可以找到统计代码,找到统计代码之后我们就可以看到 Netty 里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,就在 PlatformDepedent 这个类里面。这个地方,是一个对已使用堆外内存计数的操作,计数器为 DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的。接下来,就验证一下这个方法是否是在堆外内存分配的时候被调用。果然,在 Netty 每次分配堆外内存之前,都会计数。想到这,思路就开始慢慢清晰,而心情也开始从“秋风瑟瑟”变成“春光明媚”。阶段4:反射进行堆外内存监控CAT 上关于堆外内存的监控没有任何异常(应该是没有统计准确,一直维持在 1M),而这边我们又确认堆外内存已快超过上限,并且已经知道 Netty 底层是使用的哪个字段来统计。那么接下来要做的第一件事情,就是反射拿到这个字段,然后我们自己统计 Netty 使用堆外内存的情况。堆外内存统计字段是 DIRECT_MEMORY_COUNTER,我们可以通过反射拿到这个字段,然后定期 Check 这个值,就可以监控 Netty 堆外内存的增长情况。于是我们通过反射拿到这个字段,然后每隔一秒打印,为什么要这样做?因为,通过我们前面的分析,在爆发大量 OOM 现象之前,没有任何可疑的现象。那么只有两种情况,一种是突然某个瞬间分配了大量的堆外内存导致OOM;一种是堆外内存缓慢增长,到达某个点之后,最后一根稻草将机器压垮。在这段代码加上去之后,我们打包上线。阶段5:到底是缓慢增长还是瞬间飙升?代码上线之后,初始内存为 16384k(16M),这是因为线上我们使用了池化堆外内存,默认一个 chunk 为16M,这里不必过于纠结。但是没过一会,内存就开始缓慢飙升,并且没有释放的迹象,二十几分钟之后,内存使用情况如下:走到这里,我们猜测可能是前面提到的第二种情况,也就是内存缓慢增长造成的 OOM,由于内存实在增长太慢,于是调整机器负载权重为其他机器的两倍,但是仍然是以数K级别在持续增长。那天刚好是周五,索性就过一个周末再开看。周末之后,我们到公司第一时间就连上了跳板机,登录线上机器,开始 tail -f 继续查看日志。在输完命令之后,怀着期待的心情重重的敲下了回车键:果然不出所料,内存一直在缓慢增长,一个周末的时间,堆外内存已经飙到快一个 G 了。这个时候,我竟然想到了一句成语:“只要功夫深,铁杵磨成针”。虽然堆外内存以几个K的速度在缓慢增长,但是只要一直持续下去,总有把内存打爆的时候(线上堆外内存上限设置的是2G)。此时,我们开始自问自答环节:内存为啥会缓慢增长,伴随着什么而增长?因为我们的应用是面向用户端的WebSocket,那么,会不会是每一次有用户进来,交互完之后离开,内存都会增长一些,然后不释放呢?带着这个疑问,我们开始了线下模拟过程。阶段6:线下模拟本地起好服务,把监控堆外内存的单位改为以B为单位(因为本地流量较小,打算一次一个客户端连接),另外,本地也使用非池化内存(内存数字较小,容易看出问题),在服务端启动之后,控制台打印信息如下在没有客户端接入的时候,堆外内存一直是0,在意料之中。接下来,怀着着无比激动的心情,打开浏览器,然后输入网址,开始我们的模拟之旅。我们的模拟流程是:新建一个客户端链接->断开链接->再新建一个客户端链接->再断开链接。如上图所示,一次 Connect 和 Disconnect 为一次连接的建立与关闭,上图绿色框框的日志分别是两次连接的生命周期。我们可以看到,内存每次都是在连接被关闭的的时候暴涨 256B,然后也不释放。走到这里,问题进一步缩小,肯定是连接被关闭的时候,触发了框架的一个Bug,而且这个Bug在触发之前分配了 256B 的内存,随着Bug被触发,内存也没有释放。问题缩小之后,接下来开始“撸源码”,捉虫!阶段7:线下排查接下来,我们将本地服务重启,开始完整的线下排查过程。同时将目光定位到 netty-socketio 这个框架的 Disconnect 事件(客户端WebSocket连接关闭时会调用到这里),基本上可以确定,在 Disconnect 事件前后申请的内存并没有释放。在使用 idea debug 时,要选择只挂起当前线程,这样我们在单步跟踪的时候,控制台仍然可以看到堆外内存统计线程在打印日志。在客户端连接上之后然后关闭,断点进入到 onDisconnect 回调,我们特意在此多停留了一会,发现控制台内存并没有飙升(7B这个内存暂时没有去分析,只需要知道,客户端连接断开之后,我们断点hold住,内存还未开始涨)。接下来,神奇的一幕出现了,我们将断点放开,让程序跑完:Debug 松掉之后,内存立马飙升了!!此时,我们已经知道,这只“臭虫”飞不了多远了。在 Debug 时,挂起的是当前线程,那么肯定是当前线程某个地方申请了堆外内存,然后没有释放,继续“快马加鞭“,深入源码。其实,每一次单步调试,我们都会观察控制台的内存飙升的情况。很快,我们来到了这个地方:在这一行没执行之前,控制台的内存依然是 263B。然后,当执行完该行之后,立刻从 263B涨到519B(涨了256B)。于是,Bug 范围进一步缩小。我们将本次程序跑完,释然后客户端再来一次连接,断点打在 client.send() 这行, 然后关闭客户端连接,之后直接进入到这个方法,随后的过程有点长,因为与 Netty 的时间传播机制有关,这里就省略了。最后,我们跟踪到了如下代码,handleWebsocket:在这个地方,我们看到一处非常可疑的地方,在上图的断点上一行,调用 encoder 分配了一段内存,调用完之后,我们的控制台立马就彪了 256B。所以,我们怀疑肯定是这里申请的内存没有释放,它这里接下来调用 encoder.encodePacket() 方法,猜想是把数据包的内容以二进制的方式写到这段 256B的内存。接下来,我们追踪到这段 encode 代码,单步执行之后,就定位到这行代码:这段代码是把 packet 里面一个字段的值转换为一个 char。然而,当我们使用 idea 预执行的时候,却抛出类一个愤怒的 NPE!!也就是说,框架申请到一段内存之后,在 encoder 的时候,自己 GG 了,还给自己挖了个NPE的深坑,最后导致内存无法释放(最外层有堆外内存释放逻辑,现在无法执行到了)。而且越攒越多,直到被“最后一根稻草”压垮,堆外内存就这样爆了。这里的源码,有兴趣的读者可以自己去分析一下,限于篇幅原因,这里就不再展开叙述了。阶段8:Bug解决既然 Bug 已经找到,接下来就要解决问题了。这里只需要解决这个NPE异常,就可以 Fix 掉。我们的目标就是,让这个 subType 字段不为空。于是我们先通过 idea 的线程调用栈,定位到这个 packet 是在哪个地方定义的:我们找到 idea 的 debugger 面板,眼睛盯着 packet 这个对象不放,然后上线移动光标,便光速定位到。原来,定义 packet 对象这个地方在我们前面的代码其实已经出现过,我们查看了一下 subType 这个字段,果然是 null。接下来,解决 Bug 就很容易了。我们给这个字段赋值即可,由于这里是连接关闭事件,所以我们给他指定了一个名为 DISCONNECT 的字段(可以改天深入去研究 Socket.IO 的协议),反正这个 Bug 是在连接关闭的时候触发的,就粗暴一点了 !解决这个 Bug 的过程是:将这个框架的源码下载到本地,然后加上这一行,最后重新 Build一下,pom 里改了一下名字,推送到我们公司的仓库。这样,项目就可以直接进行使用了。改完 Bug 之后,习惯性地去 GitHub上找到引发这段 Bug 的 Commit:好奇的是,为啥这位 dzn commiter 会写出这么一段如此明显的 Bug,而且时间就在今年3月30号,项目启动的前夕!阶段9:线下验证一切准备就绪之后,我们就来进行本地验证,在服务起来之后,我们疯狂地建立连接,疯狂地断开连接,并观察堆外内存的情况:Bingo!不管我们如何断开连接,堆外内存不涨了。至此,Bug 基本 Fix,当然最后一步,我们把代码推到线上验证。阶段10:线上验证这次线上验证,我们避免了比较土的打日志方法,我们把堆外内存的这个指标“喷射”到 CAT 上,然后再来观察一段时间的堆外内存的情况:过完一段时间,堆外内存已经稳定不涨了。此刻,我们的“捉虫之旅”到此结束。最后,我们还为大家做一个小小的总结,希望对您有所帮助。总结遇到堆外内存泄露不要怕,仔细耐心分析,总能找到思路,要多看日志,多分析。如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况。逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug。熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类。最后,祝愿大家都能找到自己的“心仪已久” Bug!作者简介闪电侠,2014年加入美团点评,主要负责美团点评移动端统一长连工作,欢迎同行进行技术交流。招聘目前我们团队负责美团点评长连基础设施的建设,支持美团酒旅、外卖、到店、打车、金融等几乎公司所有业务的快速发展。加入我们,你可以亲身体验到千万级在线连接、日吞吐百亿请求的场景,你会直面互联网高并发、高可用的挑战,有机会接触到 Netty 在长连领域的各个场景。我们诚邀有激情、有想法、有经验、有能力的同学,和我们一起并肩奋斗!欢迎感兴趣的同学投递简历至 chao.yu#dianping.com 咨询。参考文献Netty 是什么Netty 源码分析之服务端启动全解析 ...

October 19, 2018 · 2 min · jiezi