sudo apt install nginx -y
sudo /etc/init.d/nginx start
sudo vim /etc/nginx/nginx.conf
upstream mihooke {
# 负载均衡的servers,默认是round robin
server 192.168.8.243:8080;
server 192.168.8.243:8090;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://mihooke;
}
}
sudo nginx -t -c /etc/nginx/nginx.conf
sudo /etc/init.d/nginx restart
sudo apt install -y mysql-client
sudo apt install -y mysql-server
ps aux | grep mysql
systemctl start mysqld
systemctl status mysql.service
sudo /etc/init.d/mysql restart
sudo vim /etc/mysql/mysql.conf.d/mysqld.conf
bind-address 改为 0.0.0.0
sudo mysql -u root -p
sudo apt install -y redis-server
sudo vim /etc/redis/redis.conf
# 增加密码登录
requirepass 行去掉注释,改成自己的密码
# 解禁本机绑定
#bind 127.0.0.1
sudo /etc/init.d/redis-server restart
# -h 主机地址 -p 端口默认6379 -a 密码
redis-cli -h 127.0.0.1 -p 6379 -a lzali
Redis和MySQL的配合使用:在MySQL中进行修改的时候,在此服务层将Redis缓存中的数据清除;用户访问时就会从MySQL中查询,查询结果再放入缓存中
需要JDK环境1.15.0
从官网下载压缩文件
tar -zxvf zookeeper-3.7.0.tar.gz
新建zoo.cfg文件
cp conf/zoo_sample.cfg conf/zoo.cfg
sudo ./bin/zkServer.sh start
# 3.6.2版本依旧会提示JAVA_HOME未设置环境变量的错误
# 可能也会提示zkEnv.sh 语法错误,原因是系统的bash不对
sudo ln -sf bash /bin/sh
再次执行就可以了
sudo ./bin/zkServer.sh status
需要Java 8+环境 此版本已内置了ZooKeeper,不必再自己安装
1.安装
tar -xzf kafka_2.13-2.7.0.tgz
cd kafka_2.13-2.7.0
2.启动zk服务
bin/zookeeper-server-start.sh config/zookeeper.properties
执行完命令窗口需要保持
3.启动kafka server
新建终端窗口,运行
bin/kafka-server-start.sh config/server.properties
2020年是不平凡的一年,对全世界,对我自己。
总的来说,2020年是在充实的无尽止业务代码中度过的。迄今为止,在软件开发行业工作已经4年半了。按工时算的话,其实也不短了,根据一万个小时定律,我理应成为这个行业的高手(高级工程师),但实际上,我并没有成为高手,而一直蹉跎于中级,并不是我学习止步了,也不是我怠慢了工作,从中级到高级需要项目的历练和平台的支撑,我想这可能就是很多工程师长时间停留在中级的原因吧。不仅如此,今年想换互联网后台服务端开发,这个方向对于做软件工程开发有更大的挑战性,面对的业务是高可用、高并发、超大的业务量等很有挑战的工作。比如开发一个支持一千个客户端的服务器和开发一个支持一百万个客户端的服务器设计和架构是完全不同的,前者可能从网上找一个demo或利用one thread per connection做法单机即可实现,但后者工作量可远远不止前者那么简单,后者一定是跑在分布式系统上的,一旦涉及到分布式就必须考虑其CAP(强一致性、高可用、分区容错),但同时满足CAP是不现实的,实际工程中一般根据业务满足BASE(基本可用、软状态、最终一致性)。所以考虑再三,决定从现在的传统软件开发方向转互联网后台开发方向,虽然之前的经验不能完全复制过去,但最起码开发语言(C++以及Python)的使用可以拿来用,还有解决问题的能力。
总结过去是一个很好的习惯。过去一年做的主要事情是完善自己的技能图谱,根据图谱知识点针对性的补习。主要包括:
首先,基础知识是不能被遗忘的,即便进行了很多年的业务开发。重新巩固了数据结构原理、操作系统知识、网络协议(主要是TCP和HTTP),巩固过程中加深了对这些基础知识的理解,甚至纠正了之前的一些误区,收获很大。作为工作了近5年的工程师,不能只会调用库,还需要理解库的实现原理。我们使用库是为了更高效地开发,防止重复造轮子;我们理解其原理是为了拥有造轮子的能力,因为在变幻莫测的复杂业务中,很可能出现某个轮子不合适,此时就需要我们改造为自己所用了。
其次,工程能力,当我们写小体量代码的时候通常很随意,但工程代码逐渐增多的时候,模块化设计和分层设计便派上用场了。工程能力全靠在实际工作中锻炼了,当然也可以看一些优秀的开源库,比如muduo,bRPC等。
我们的工作是做不完的,可能会同时有若干件事情需要处理,但人的大脑毕竟是单核的,同时只能做一件事情,如果每件事都做一半来回切换,还得在大脑中保存其上下文,反而效率不高,那必然挑其中紧急且重要的一件事先做。紧急和重要的界限要把握清楚,这在工作中体现尤为重要。通常我会优先解决紧急的事,然后给重要的事留下足够的大块时间来处理,这样在重要事情的处理上不会慌乱。学习同样也是如此,应该抓住本质,有所侧重点,这样能锻炼自己解决问题抓住本质的能力。
自制力与兴趣的坚持,要说人都是有惰性的,如何保持对一件事或工作的热情呢?答案就是自己的兴趣。在兴趣的道路上,最大的绊脚石是自身的自制力不够强大。这一点对于过去一年,我深有感触。C++后台开发路线并不像前端那样可以做出来酷炫的页面来展示自己的成果,也不像嵌入式开发那样可以控制实体设备做有趣的事。实际上后台开发基本都是黑窗口程序,看不到任何界面,调试完全根据日志和数据,似乎不那么有趣,所以后台开发必须是自己感兴趣才能坚持下去。也许过个一年半载,自己的斗志和激情会磨灭,但只要在自己信心动摇时,保持自制力,坚持下去,终会迎接到自己成为高手那一刻。有时候想一想这么多台机器共同协作完成后台服务的提供,难道不是很有趣吗?
黑胡子所言:
人的梦想是不会终止的!
未来3年,我期望能够在后台开发领域驰骋,在级别上能够达到高级工程师,提高实际解决问题的能力,进一步提高工程能力。近些年程序员35岁危机在网上很热,起源于华为裁掉了部分超过35岁的技术支持工程师,传到网上后开始发酵以及以讹传讹,很多人担心自己到35岁后被公司优化掉。但实际上,有些公司优化大龄程序员是优化的那些能力没有提升的,或是技术类支持的,工作很容易被替代的。一句话讲就是能力与工作年限严重不匹配的工程师会被淘汰。我不希望我自己在若干年后变成这样,我也不认为写代码就是低等繁重的工作,反而写代码有很多乐趣。不知何时起,国内有了工作N年以上的程序员还在写代码就是能力不强的论调,我认为这种风气是很不好的,我相信很多程序员都有相同的观点,这是热爱技术的一个很重要的表现。
大家都知道身体是革命本钱,有多少人想健身但最终因为各种各样的原因放弃了,制定的计划屡屡被取消,很难吗?最重要的是不够坚持罢了。新的一年,继续坚持锻炼。知乎上的《有哪些让你受益的好习惯》,很推荐看看,强迫自己养成一些好习惯,真的受益终身。
近期阅读了muduo网络库和brpc远程调用库,从中学到了非常多的知识,专门来记录一下。
muduo是一个基于epoll(LT)的IO复用reactor模式的网络库,代码风格是基于std::bind+std::function对象的回调风格,很C++11。可以利用它轻松构建一个高性能的服务器程序,主要思想是one loop per thread,即epoll事件监控交给一个单独loop,此loop也称为事件分发器(Event Dispatcher),每个连接(Channel)交给单独一个loop,不存在跨线程操作同一个socket,大大减少了竞态(Race Condition)。库中还提供了服务器一些常用组件的经典实现,比如定时器(Timer)、线程池(ThreadPool)、线程安全的单例(Singleton)、异步写日志(Logging),源码很值得初学者进行研究。我自己为了弄懂reactor+one loop per thread + threadpool机制,从只有一个main函数中调用epoll到一步一步实现了muduo的reactor框架,代码详见这里,这个仓库里也保存了一份muduo代码,里面加了我自己对muduo的理解。muduo库结合作者的书看效果更佳。作者还在B站有一期《网络编程实战》的课程,是对muduo代码及示例的解读,很值得一看。
值得一提的是,muduo的多线程模型是reactors+threadpool,注意这里是reactors,即多个reactor。主线程是一个负责accept的主reactor,当主线程accept到连接时,便均匀地放到threadpool里负责的子reactors中,比如threadpool大小是4,则有4个子reactors,分别运行在4个EventLoop中,某时刻收到连接A B C D,则分别放入4个EventLoop中,这样每个reactor负责一个连接,再收到连接时,根据Round-Robin规则,放到对应reactor中,每一个连接的生命周期由当前的EventLoop来维护,也减少了race-condition。缓存数据库memcached也是这个模型。还有另一种IO模型:recator+多进程/多线程,即accept和新连接都放入同一个reactor中,读写事件放到新的进程或线程中处理。至于哪种模型好,只能说各有利弊。像redis就是这种模型的简化版,所有事件都在IO线程中处理,原因是redis操作是纯内存操作,几乎不会有耗时操作,也不会有长时间CPU操作,在一个线程中操作也省了锁的争用。至于多进程和多线程有何区别,我认为对外提供一个服务,首选多线程,毕竟系统调度的基本单位是线程,可比进程节省些资源,如果考虑多线程之间不存在共享资源,或者考虑更稳定的服务,可考虑多进程,理由是假设某个服务掉线,进程的话可以直接重启来恢复服务,线程的话就没办法在用户不感知的情况下重启恢复了,因为进程重启,进程内的所有线程都没了。
brpc是一个基于epoll(ET)的RPC库,兼容很多主流RPC协议,主打高性能。笔者当初选择brpc学习的一个主要原因是brpc的文档写的非常详细,并且部分文档还介绍了分布式服务器开发中常见的模式,难点,痛点以及如何解决它们,知识覆盖面很广,几乎囊括了后端服务器开发个各个方面,可谓是良心之作,一个真正的工业库,阅读其源码能深有这种感受。另一个优秀的RPC库grpc,Google出品,比brpc稍早开源的,grpc的协议使用HTTP2,RPC架构是基于protobuf3,整体上来说,grpc库要更前沿一些,但它并没有实现分布式系统,这也是我选择brpc学习的最大原因。由于brpc使用了protobuf2的协议,因此整个RPC通信架构也是基于protobuf2的,代码风格是经典的继承式面向对象。
brpc还大量造了很多轮子,有N:M线程模型的用户态线程(即协程)bthread,有内存方面的ResourcePool/ObjectPool,原子操作的,文件操作的,应用层多生产者单消费者无锁队列,Work-Staling ThreadPool,各种改进的数据结构。印象特别深的是定时器(TimerThread),由于RPC中定时器场景和一般服务器中场景不太一样,比如一般服务器中定时器场景都是定时回调注册函数,注意这里是这些回调函数都会去执行;但RPC中就不一样了,基本上每一次RPC调用都会设置timeout,而绝大多数RPC调用都不会timeout,那么系统在RPC调用结束后还得负责从内存中把无效的注册函数删掉,如此做法在RPC中属于无用功。muduo中的定时器实现就是把注册回调保存在红黑树上,频繁操作时间复杂度也是O(lgN),brpc从两个方面来提升,第一是降低锁的调用,通过把注册回调散列到多个bucket中来降低线程间的竞争;第二是不进行或很少进行删除操作,需要删除时通过修改标记位,并且删除操作不参与全局竞争,这么做相当于定制了一版RPC场景的定时器。
brpc中这样的情况很多,完全是根据场景业务定制的开发,当然,这里的场景是分布式的高并发场景。服务发现(Service Discovery)、负载均衡(Load Balancing)、熔断机制(Circuit Breaker)、一致性哈希(Consistent Hashing)、限流(Concurrency Limiter)等都可以找到典型的解决办法。
这里简单谈一下我对协程的理解,我最开始了解协程是Python中的协程,但Python的协程一开始需要自己用yield关键字来实现,3.5之后需要借助第三方库async和await来实现,仍然是N:1的线程模型,即N个协程在同一个线程中运行,这样有一个弊端,就是其中某一个协程阻塞,其他协程也就阻塞了,毕竟协程的调度是由用户自己来调度的,目的就是节省系统层面的昂贵耗时的上下文切换。bthread不一样,它是N:M线程模型,即N个协程可运行在M个线程中,Golang也是这样的线程模型,做到真正的协程并发。bthread毕竟是C++语言实现的,和Golang的croutine是不一样的,bthread是用汇编来实现协程的上下文切换的(原版是boost中实现),并在此基础上封装了work-stealing pool,在应用层来调度协程运行,当某个线程中没有协程任务时,可从其他任务多的线程中窃取任务协程到自己,当然,这其中细节很多,比如如何窃取及窃取多少。
两个库都是实现的后端server,要承受高性能并发请求的业务场景,可以发现它们各自都有实现一些基础库,比如任务队列、定时器、线程池、IO缓冲区。其实服务端组件常用的也就那么几种,只是业务场景不同,需要根据业务场景实现一个高效的组件,毕竟任何脱离业务的设计都是耍流氓。
近期抽空整理了自己的技能图谱,我大致分了以下几类:
由于图谱是在笔记中整理的,可点击链接跳转查看,初步整理,知识点还不太完善,会持续更新。
先来看一下条件变量的一般用法:
// 等待端:线程 t1
{
lock_guard(&mutex);
while (!满足唤醒的目的)
cond.wait();
}
// 唤醒端:线程 t2
{
lock_guard(&mutex);
cond.notify();
}
以上就是条件变量的一般用法。这里有几点值得我们注意。
Linux下pthread_cond 的用法如上,C++11对于虚假唤醒有了新的用法:
// wait有重载版本
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
// 使用
bool couldWeakup{false};
{
std::unique_lock<std::mutex>& lock(mutex);
cond.wait(lock, []{ return couldWeakup; });
}
{
std::lock_guard<std::mutex> lk(mutex);
couldWeakup = true;
cond.notify_all();
}
wait()的第二个参数表示:等待是否应该继续持续,如果是false,则继续等待。等价于while循环写法。
二叉树的遍历
二叉树的遍历操作实际中很少使用,二叉树的特性通常更适合进行查找。遍历分为先序,中序,后序和层次遍历。
先序遍历指先遍历父节点,再依次遍历左孩子和右孩子;中序遍历指先遍历左孩子,再依次遍历父节点和右孩子;后序遍历指先遍历右孩子,再依次遍历左孩子和父节点;层次遍历指从根节点开始,按照层级遍历节点。
遍历的实现当中,可分为递归实现和非递归实现。递归实现思路简单,非递归实现巧妙地利用了stack和queue的特性,值得学习。
template <typename T>
struct TreeNode
{
TreeNode *left;
TreeNode *right;
T data;
};
/// 递归遍历
/// 先序
void preOrder(const TreeNode *t)
{
if (!t) return;
cout << t->data << endl;
preOrder(t->left);
preOrder(t->right);
}
/// 中序
void inOrder(const TreeNode *t)
{
if (!t) return;
inOrder(t->left);
cout << t->data << endl;
inOrder(t->right);
}
/// 后序
void postOrder(const TreeNode *t)
{
if (!t) return;
postOrder(t->left);
postOrder(t->right);
cout << t->data << endl;
}
/// 非递归遍历
/// 先序
void preOrder(const TreeNode *t)
{
TreeNode *p = t;
stack<TreeNode*> s;
while (p || !s.empty())
{
if (p)
{
cout << p->data << endl;
s.push(p);
p = p->left;
}
else
{
p = s.top();
s.pop();
p = p->right;
}
}
}
/// 中序
void inOrder(const TreeNode *t)
{
TreeNode *p = t;
stack<TreeNode*> s;
while (p || !s.empty())
{
if (p)
{
s.push(p);
p = p->left;
}
else
{
p = s.top();
s.pop();
cout << p->data << endl;
p = p->right;
}
}
}
/// 后序
void postOrder(const TreeNode *t)
{
TreeNode *cur = t;
TreeNode *prevVisited = nullptr;
stack<TreeNode*> s;
while (cur || !s.empty())
{
while (cur)
{
s.push(cur);
cur = cur->left;
}
cur = s.top();
if (!cur->right || cur->right == prevVisited)
{
cout << cur->data << endl;
prevVisited = cur->right;
s.pop();
cur = nullptr;
}
else
cur = cur->right;
}
}
/// 层次遍历
void layerOrder(const TreeNode *t)
{
TreeNode *p = t;
queue<TreeNode*> q;
q.push(p);
while (!q.empty())
{
p = q.front();
q.pop();
cout << p->data << endl;
if (p->left) q.push(p->left);
else if (p->right) q.push(p->right);
}
}