自我介绍
+oifjwoiej
+diff --git a/404.html b/404.html new file mode 100644 index 00000000..555ea79f --- /dev/null +++ b/404.html @@ -0,0 +1,46 @@ + + +
+ + + + + + +留空
oifjwoiej
各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。\\n大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。
","autoDesc":true}');export{e as data}; diff --git "a/assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-X_ZVzsKc.js" "b/assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-X_ZVzsKc.js" new file mode 100644 index 00000000..288afd89 --- /dev/null +++ "b/assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-X_ZVzsKc.js" @@ -0,0 +1 @@ +import{_ as l}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as o,o as i,c as a,a as e,d as t,b as n,e as s}from"./app-cS6i7hsH.js";const h={},c=e("h1",{id:"_1-历史与架构",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-历史与架构","aria-hidden":"true"},"#"),t(" 1.历史与架构")],-1),d={href:"http://www.wenzhihuai.com/",target:"_blank",rel:"noopener noreferrer"},_={href:"https://github.com/Zephery/newblog",target:"_blank",rel:"noopener noreferrer"},g=e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/600-20240126113210252.png",alt:"",tabindex:"0",loading:"lazy"}),e("figcaption")],-1),p={href:"http://www.wenzhihuai.com/weibonlp.html",target:"_blank",rel:"noopener noreferrer"},b=e("br",null,null,-1),u={href:"https://github.com/Zephery/newblog/blob/master/doc/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.md",target:"_blank",rel:"noopener noreferrer"},f=e("br",null,null,-1),m={href:"https://github.com/Zephery/newblog/blob/master/doc/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.md",target:"_blank",rel:"noopener noreferrer"},w=e("br",null,null,-1),E={href:"https://github.com/Zephery/newblog/blob/master/doc/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.md",target:"_blank",rel:"noopener noreferrer"},B=e("br",null,null,-1),x={href:"https://github.com/Zephery/baidutongji/blob/master/README.md",target:"_blank",rel:"noopener noreferrer"},k=e("br",null,null,-1),y={href:"https://github.com/Zephery/newblog/blob/master/doc/6.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.md",target:"_blank",rel:"noopener noreferrer"},z=e("br",null,null,-1),v=e("li",null,[t("[数据库]"),e("br")],-1),A=e("li",null,[t("使用机器学习对微博进行分析"),e("br")],-1),C=e("li",null,[t("网站性能优化"),e("br")],-1),P=e("li",null,[t("SEO优化"),e("br")],-1),S=e("h2",{id:"_1-建站故事与网站架构",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-建站故事与网站架构","aria-hidden":"true"},"#"),t(" 1.建站故事与网站架构")],-1),V=e("h3",{id:"_1-1建站过程",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-1建站过程","aria-hidden":"true"},"#"),t(" 1.1建站过程")],-1),I={href:"http://www.yangqq.com/",target:"_blank",rel:"noopener noreferrer"},j=e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/500.png",alt:"",tabindex:"0",loading:"lazy"}),e("figcaption")],-1),H=e("p",null,"第二版的界面确实是这样的,只是把图片的切换变成了wowslider,也是简单的用bootstrap和pagehelper做了下分页,现在的最终版保留了它的header,然后评论框使用了多说(超级怀念多说)。后端也由原来的ssh变成了ssm,之后加上了lucene来对文章进行索引。之后,随着多说要关闭了,突然之间有很多div都不适应了(我写死了的。。。),再一次,没法看,不想看,一怒之下再次推翻重做,变成了现在这个版本。",-1),M={href:"https://tale.biezhi.me",target:"_blank",rel:"noopener noreferrer"},Z={href:"https://yusi123.com",target:"_blank",rel:"noopener noreferrer"},F=e("h3",{id:"_1-2-网站整体技术架构",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-2-网站整体技术架构","aria-hidden":"true"},"#"),t(" 1.2 网站整体技术架构")],-1),q=e("p",null,"最终版的技术架构图如下:",-1),G=e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/awfawefwefwef.png",alt:"",tabindex:"0",loading:"lazy"}),e("figcaption")],-1),J={href:"http://jinnianshilongnian.iteye.com/blog/1594806",target:"_blank",rel:"noopener noreferrer"},R=s('运行流程分析
日志系统架构如下:
',6),D={href:"http://www.wenzhihuai.com/log.html",target:"_blank",rel:"noopener noreferrer"},L=e("br",null,null,-1),N=e("h3",{id:"_1-4-【有点意思】自然语言处理",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-4-【有点意思】自然语言处理","aria-hidden":"true"},"#"),t(" 1.4 【有点意思】自然语言处理")],-1),T={href:"http://www.wenzhihuai.com/weibonlp.html",target:"_blank",rel:"noopener noreferrer"},U=e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/QQ截图20170825141127.png",alt:"",tabindex:"0",loading:"lazy"}),e("figcaption")],-1),K=e("h2",{id:"总结",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#总结","aria-hidden":"true"},"#"),t(" 总结"),e("br")],-1),Q={href:"https://github.com/Zephery/newblog",target:"_blank",rel:"noopener noreferrer"};function X(O,W){const r=o("ExternalLinkIcon");return i(),a("div",null,[c,e("p",null,[t("各位大佬瞄一眼"),e("a",d,[t("我的个人网站"),n(r)]),t("呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址"),e("a",_,[t("https://github.com/Zephery/newblog"),n(r)]),t(" 。 大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。")]),g,e("p",null,[t("由原本的ssh变成ssm,再变成ssm+shiro+lucene,到现在的前后台分离。前台使用ssm+lucene,后台使用spring boot+shiro。其中,使用pagehelper作为分页,lucene用来搜索和自动补全,使用百度统计的API做了个日志系统,统计pv和uv什么的,同时,还有使用了JMX来观察JVM的使用和cpu的使用率,机器学习方面,使用了adaboost和朴素贝叶斯对微博进行分类,有兴趣的可以点点"),e("a",p,[t("有点意思"),n(r)]),t("这个页面。 本文从下面这几个方面来讲讲网站的建立:"),b]),e("ol",null,[e("li",null,[e("a",u,[t("建站故事与网站架构"),n(r)]),f]),e("li",null,[e("a",m,[t("lucene搜索的使用"),n(r)]),w]),e("li",null,[e("a",E,[t("使用quartz来定时备份数据库"),n(r)]),B]),e("li",null,[e("a",x,[t("使用百度统计api做日志系统"),n(r)]),k]),e("li",null,[e("a",y,[t("Nginx小集群的搭建"),n(r)]),z]),v,A,C,P]),S,V,e("p",null,[t("起初,是因为学习的时候老是找不到什么好玩而又有挑战性的项目,看着struts、spring、hibernate的书,全都是一些小项目,做了感觉也没啥意义,有时候在博客园看到别人还有自己的网站,特羡慕,最终就选择了自己做一个个人网站。期初刚学的ssh,于是开始了自己的ssh搭建个人网站的行程,但是对于一个后端的人来说,前端是个大问题啊。。。。所以初版的时候差不多全是纯生的html、css、js,至少发博客这个功能实现了,但是确实没法看。前前后后折腾了一个多月,决定推翻重做,到百度看看别人怎么做的。首先看到的是"),e("a",I,[t("杨青"),n(r)]),t("的网站,已经好几年没更新了,前端的代码看起来比较简单,也是自己能够掌握的,但是不够美观,继续找,在模板之家发现了一个高大上的模板。")]),j,H,e("p",null,[t("最终版本在考虑时,也找了很多模板,影响深刻的是"),e("a",M,[t("tale"),n(r)]),t("和"),e("a",Z,[t("欲思"),n(r)]),t("这两个主题,期中,tale使用的是java语言写的,刚知道的那一刻我就没好感了,java后端我是要自己全部写的,tale这个页面简洁但是不够炫,而且内容量太低,可能就只是个纯博客,之后发现了欲思,拓展性强,只可惜没有静态的版本,后台是纯生的PHP(嗯,PHP是世界上最好的语言),看了看,没事,保存网页,前端自己改,后端自己全部重写,最终变成了现在这个版本,虽然拼接的时候各种css、js混入。。。。还好在做网站性能优化的时候差不多全部去掉了。最终版加入redis、quartz、shiro等,还有python机器学习、flask的restful api,可谓是大杂烩了。 页面看着还算凑合,至少不是那种看都看不过去的那种了,但是仔细看看,还是有不少问题的,比如瀑布流,还有排版什么的。只能等自己什么时候想认真学学前端的东西了。 已经部署在腾讯云服务器上,存储使用了七牛云的cdn。")]),F,q,G,e("p",null,[t("网站核心主要采用Spring SpringMVC和Mybatis,下图是当访问一篇博客的时候的运行流程,参考了"),e("a",J,[t("张开涛"),n(r)]),t("的博客。")]),R,e("p",null,[e("a",D,[t("日志系统"),n(r)]),t("曾经尝试采用过ELK,实时监控实在是让人不能不称赞,本地也跑起来了,但是一到服务器,卡卡卡,毕竟(1Ghz CPU、1G内存),只能放弃ELK,采用百度统计。百度统计提供了Tongji API供开发者使用,只是有次数限制,2000/日,实时的话实在有点低,只能统计前几天的PV、UV等开放出来。其实这个存放在mysql也行,不过这些零碎的数据还是放在redis中,方便管理。 出了日志系统,自己对服务器的一些使用率也是挺关心的,毕竟服务器配置太低,于是利用了使用了tomcat的JMX来对CPU和jvm使用情况进行监控,这两个都是实时的。出了这两个,对内存的分配做了监控,Eden、Survivor、Tenured的使用情况。"),L]),N,e("p",null,[t("本人大学里的毕业设计就是基于AdaBoost算法的情感分类,学到的东西还是要经常拿出来看看,要不然真的浪费了我这么久努力做的毕业设计啊。构建了一个基本的情感分类小系统,每天抓取微博进行分类存储在MySql上,并使用flask提供Restful API给java调用,可以点击"),e("a",T,[t("这里"),n(r)]),t("尝试(请忽略Google的图片)。目前分类效果不是很明显,准确率大概只有百分之70%,因为训练样本只有500条(找不到训练样本),机器学习真的太依赖样本的标注。这个,只能请教各位路人大神指导指导了。")]),U,K,e("p",null,[t("断断续续,也总算做了一个能拿得出手仍要遮遮掩掩才能给别人看的网站,哈哈哈哈哈,也劳烦各位帮忙找找bug。前前后后从初学Java EE就一直设想的事也还算有了个了结,以后要多多看书,写点精品文章。PS:"),e("a",Q,[t("GitHub上求给个Star"),n(r)]),t(",以后面试能讲讲这个网站。")])])}const ee=l(h,[["render",X],["__file","1.历史与架构.html.vue"]]);export{ee as default}; diff --git "a/assets/10.\347\275\221\347\253\231\351\207\215\346\236\204.html-8BbyrcnW.js" "b/assets/10.\347\275\221\347\253\231\351\207\215\346\236\204.html-8BbyrcnW.js" new file mode 100644 index 00000000..bb0ee77d --- /dev/null +++ "b/assets/10.\347\275\221\347\253\231\351\207\215\346\236\204.html-8BbyrcnW.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-523e9724","path":"/personalWebsite/10.%E7%BD%91%E7%AB%99%E9%87%8D%E6%9E%84.html","title":"10.网站重构","lang":"zh-CN","frontmatter":{"description":"10.网站重构 因原本的","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/personalWebsite/10.%E7%BD%91%E7%AB%99%E9%87%8D%E6%9E%84.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"10.网站重构"}],["meta",{"property":"og:description","content":"10.网站重构 因原本的"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"10.网站重构\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706182936000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.03,"words":9},"filePathRelative":"personalWebsite/10.网站重构.md","localizedDate":"2024年1月25日","excerpt":"因原本的
\\n","autoDesc":true}');export{e as data}; diff --git "a/assets/10.\347\275\221\347\253\231\351\207\215\346\236\204.html-vlubYdUg.js" "b/assets/10.\347\275\221\347\253\231\351\207\215\346\236\204.html-vlubYdUg.js" new file mode 100644 index 00000000..724aac62 --- /dev/null +++ "b/assets/10.\347\275\221\347\253\231\351\207\215\346\236\204.html-vlubYdUg.js" @@ -0,0 +1 @@ +import{_}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as t,c as o,a as e,d as a}from"./app-cS6i7hsH.js";const c={},r=e("h1",{id:"_10-网站重构",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_10-网站重构","aria-hidden":"true"},"#"),a(" 10.网站重构")],-1),s=e("p",null,"因原本的",-1),n=[r,s];function d(i,l){return t(),o("div",null,n)}const m=_(c,[["render",d],["__file","10.网站重构.html.vue"]]);export{m as default}; diff --git a/assets/1mysql.html-OHVRcqVz.js b/assets/1mysql.html-OHVRcqVz.js new file mode 100644 index 00000000..892c8876 --- /dev/null +++ b/assets/1mysql.html-OHVRcqVz.js @@ -0,0 +1 @@ +import{_ as t}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as s,c as a,a as e,d as o}from"./app-cS6i7hsH.js";const n="/assets/img-kuzmmtyE.png",c={},i=e("h1",{id:"mysql",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#mysql","aria-hidden":"true"},"#"),o(" mysql")],-1),r=e("figure",null,[e("img",{src:n,alt:"img",tabindex:"0",loading:"lazy"}),e("figcaption",null,"img")],-1),l=[i,r];function _(m,d){return s(),a("div",null,l)}const u=t(c,[["render",_],["__file","1mysql.html.vue"]]);export{u as default}; diff --git a/assets/1mysql.html-iiTeieOx.js b/assets/1mysql.html-iiTeieOx.js new file mode 100644 index 00000000..c91affc3 --- /dev/null +++ b/assets/1mysql.html-iiTeieOx.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-aa93dec0","path":"/database/mysql/1mysql.html","title":"mysql","lang":"zh-CN","frontmatter":{"description":"mysql img","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/1mysql.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"mysql"}],["meta",{"property":"og:description","content":"mysql img"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T11:42:16.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-25T11:42:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"mysql\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T11:42:16.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706092222000,"updatedTime":1706182936000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.01,"words":3},"filePathRelative":"database/mysql/1mysql.md","localizedDate":"2024年1月24日","excerpt":"倒排索引:将文档中的词作为关键字,建立词与文档的映射关系,通过对倒排索引的检索,可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。
停用词:在一段句子中,去掉之后对句子的表达意向没有印象的词语,如“非常”、“如果”,中文中主要包括冠词,副词等。
排序:搜索引擎在对一个关键词进行搜索时,可能会命中许多文档,这个时候,搜索引擎就需要快速的查找的用户所需要的文档,因此,相关度大的结果需要进行排序,这个设计到搜索引擎的相关度算法。
索引的建立
索引的搜索
注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一样。首先需要导入lucene所使用的包。使用maven:
<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-core</artifactId><!--lucene核心-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-analyzers-common</artifactId><!--分词器-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-queryparser</artifactId><!--格式化-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
+ <version>${lucene.version}</version>
+</dependency>
+
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存储的位置
+SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器
+IndexWriterConfig config = new IndexWriterConfig(analyzer);
+IndexWriter writer = new IndexWriter(dir, config);
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //对标题做索引
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引
+writer.addDocument(doc);
+writer.close();
+
IndexWriter writer = getWriter();
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
+writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc); //更新索引
+writer.close();
+
private static void search_index(String keyword) {
+ try {
+ Directory dir = FSDirectory.open(Paths.get("blog_index")); //获取要查询的路径,也就是索引所在的位置
+ IndexReader reader = DirectoryReader.open(dir);
+ IndexSearcher searcher = new IndexSearcher(reader);
+ SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+ QueryParser parser = new QueryParser("content", analyzer); //查询解析器
+ Query query = parser.parse(keyword); //通过解析要查询的String,获取查询对象
+ TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中,
+ for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+ Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+ System.out.println(doc.get("title")); //fullPath是刚刚建立索引的时候我们定义的一个字段
+ }
+ reader.close();
+ } catch (IOException | ParseException e) {
+ logger.error(e.toString());
+ }
+}
+
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
+SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
+Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
+highlighter.setTextFragmenter(fragmenter);
+for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+ Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+ String title = doc.get("title");
+ TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
+ String hTitle = highlighter.getBestFragment(tokenStream, title);
+ System.out.println(hTitle);
+}
+
结果
<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
+
ScoreDoc lastBottom = null;//相当于pageSize
+BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
+QueryParser parser1 = new QueryParser("title", analyzer);//对文章标题进行搜索
+Query query1 = parser1.parse(q);
+booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
+TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits); //lastBottom(pageSize),pagehits(pagenum)
+
<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-suggest</artifactId>
+ <version>6.6.0</version>
+</dependency>
+
public class Blog implements Serializable {
+
InputIterator的几个方法:
+long weight():返回的权重值,大小会影响排序,默认是1L
+BytesRef payload():对某个对象进行序列化
+boolean hasPayloads():是否有设置payload信息
+Set<BytesRef> contexts():存入context,context里可以是任意的自定义数据,一般用于数据过滤
+boolean hasContexts():判断是否有下一个,默认为false
+
public class BlogIterator implements InputIterator {
+ /**
+ * logger
+ */
+ private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
+ private Iterator<Blog> blogIterator;
+ private Blog currentBlog;
+
+ public BlogIterator(Iterator<Blog> blogIterator) {
+ this.blogIterator = blogIterator;
+ }
+
+ @Override
+ public boolean hasContexts() {
+ return true;
+ }
+
+ @Override
+ public boolean hasPayloads() {
+ return true;
+ }
+
+ public Comparator<BytesRef> getComparator() {
+ return null;
+ }
+
+ @Override
+ public BytesRef next() {
+ if (blogIterator.hasNext()) {
+ currentBlog = blogIterator.next();
+ try {
+ //返回当前Project的name值,把blog类的name属性值作为key
+ return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 将Blog对象序列化存入payload
+ * 可以只将所需要的字段存入payload,这里对整个实体类进行序列化,方便以后需求,不建议采用这种方法
+ */
+ @Override
+ public BytesRef payload() {
+ try {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bos);
+ out.writeObject(currentBlog);
+ out.close();
+ BytesRef bytesRef = new BytesRef(bos.toByteArray());
+ return bytesRef;
+ } catch (IOException e) {
+ logger.error("", e);
+ return null;
+ }
+ }
+
+ /**
+ * 文章标题
+ */
+ @Override
+ public Set<BytesRef> contexts() {
+ try {
+ Set<BytesRef> regions = new HashSet<BytesRef>();
+ regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
+ return regions;
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Couldn't convert to UTF-8");
+ }
+ }
+
+ /**
+ * 返回权重值,这个值会影响排序
+ * 这里以产品的销售量作为权重值,weight值即最终返回的热词列表里每个热词的权重值
+ */
+ @Override
+ public long weight() {
+ return currentBlog.getHits(); //change to hits
+ }
+}
+
/**
+ * ajax建立索引
+ */
+@Override
+public void ajaxbuild() {
+ try {
+ Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+ SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+ AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+ //创建Blog测试数据
+ List<Blog> blogs = blogMapper.getAllBlog();
+ suggester.build(new BlogIterator(blogs.iterator()));
+ } catch (IOException e) {
+ System.err.println("Error!");
+ }
+}
+
@Override
+public Set<String> ajaxsearch(String keyword) {
+ try {
+ Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+ SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+ AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+ List<String> list = lookup(suggester, keyword);
+ list.sort(new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ if (o1.length() > o2.length()) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ });
+ Set<String> set = new LinkedHashSet<>();
+ for (String string : list) {
+ set.add(string);
+ }
+ ssubSet(set, 7);
+ return set;
+ } catch (IOException e) {
+ System.err.println("Error!");
+ return null;
+ }
+}
+
@RequestMapping("ajaxsearch")
+public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ String keyword = request.getParameter("keyword");
+ if (StringUtils.isEmpty(keyword)) {
+ return;
+ }
+ Set<String> set = blogService.ajaxsearch(keyword);
+ Gson gson = new Gson();
+ response.getWriter().write(gson.toJson(set));//返回的数据使用json
+}
+
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
+<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
+<script type="text/javascript">
+ /******************** remote start **********************/
+ $('#remote_input').autocomplete({
+ source: [
+ {
+ url: "ajaxsearch.html?keyword=%QUERY%",
+ type: 'remote'
+ }
+ ]
+ });
+ /********************* remote end ********************/
+</script>
+
首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。
","autoDesc":true}');export{e as data}; diff --git a/assets/2017.html-BbxR5q6z.js b/assets/2017.html-BbxR5q6z.js new file mode 100644 index 00000000..93ee87aa --- /dev/null +++ b/assets/2017.html-BbxR5q6z.js @@ -0,0 +1 @@ +import{_ as r}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as o,o as g,c as h,a,d as i,b as n,e as t}from"./app-cS6i7hsH.js";const p={},c=a("h1",{id:"_2017",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#_2017","aria-hidden":"true"},"#"),i(" 2017")],-1),s=a("p",null,[i("2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。"),a("br"),i(" 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?")],-1),d=a("figure",null,[a("img",{src:"http://image.wenzhihuai.com/images/20171231044153.png",alt:"",tabindex:"0",loading:"lazy"}),a("figcaption")],-1),l=a("p",null,"回到学校,待了几天,那个整天给别人灌毒鸡汤的人还是不着急找工作:该有的时候总会有的啦,然后继续打游戏、那个天天喊着吃麻辣香锅的人,记忆中好像今年都没有吃上,哈哈、厕所还是那样偶尔炸一下,嗯,感觉一切都很熟悉,只是,我有了点伤感,毕竟身边又要换一群不认识的人了。考完试,终于能回广东了,一年没回了,开心不得了,更多的是,为了那个喜欢了好几年没有表白的妹纸,之前从没想过真的能每天跟一个妹纸道晚安,秒回,但是,对她,我还真做到了。1月份的广州,不热不冷,空气很清新,比北京、天津那股泥土味好多了,只是,我被拒了,男闺蜜还是男闺蜜,备胎还是备胎,那会还没想过放弃,直到...",-1),u={href:"http://www.wenzhihuai.com",target:"_blank",rel:"noopener noreferrer"},f=t('2月份底开始慢慢投简历,跑招聘会,深圳招聘的太少了,那就去广州,大学城,于是,开始了自己一周两天都在广深往返。有的公司笔试要跑一次广州,一面要跑一次,二面还要跑一次,每次看着路费,真的承受不起,但是有希望就行。乐观之下,到来的是一重又一重的打击。
**数码,很看好的公司,在广州也算是老品牌了,笔试的时候面试官没有带够卷子,就拿了大数据的做,还过了,面试的时候被面到union和union all的区别,答反了,还有left join和right join的原理,一脸懵逼,过后去了百度搜了搜,这东西哪来的原理?
然后是个企业建站的,笔试完了,面试官照着题问然后没说啥就让我走了,额,我好像还没自我介绍呢。
有点想回北京了,往北京投了一份,A轮公司,月薪10k,过了,觉得自己技术应该不差吧,不过还是拒了,因为一想起那里满脑子都是雾霾。
没事没事,下一家,某游戏公司,笔试过了,一面也过了,然后被问flume收集的时候断网处理机制、MD5和RSA原理、Angulajs和Vuejs用没用过、千万级的数据是如何存储的?额,跟你们想的一样,我一道也答出来,然后就挂了,面试官跟我说,你要是来早三个月这些都会啦,说的我很紧张,广东的同学都那么强么?
这个时候还没质疑自己的技术,去了趟深圳某家小公司,金融的,HR问我是不是培训过来的,心里开始突然间好苦涩,大学花那么多时间学习居然被别人用来和培训机构的人相提并论,笔试不知从哪整来的英文逻辑题,做完了然后上机笔试,做不出来不能见面试官,开始感觉这公司有点。。。。还是撤了撤了。
突然感觉面试面的绝望,是自己的问题么?是自己技术太菜了么?开始怀疑自己,感觉也是,已经3月份底了,毕竟别人都在实习,而我已经离职3个月没工作了,但是那些问题不是有点离谱了,一开始是开始心态爆炸觉得怎么可以对应届生问这些问题,不应该是基础么?算法什么的,后来是无奈了,开始上网搜各种不沾边的技术。
还有其他的面试。。。有的工资不高(6K不到),有的面试问题全答对就因为说了有点想做大数据就不要你,有的聊得很欢快却没消息,有的说招java但是笔试一道java题都没有全是前端的,有的打着互联网实际却是外包的公司。。。。 4月初真的很绝望很绝望,不想跟别人说话,是不是很可笑,一个从北京B+轮的公司回广东,面试近15个,居然只能拿到2个offer,其中之一国企外包的,都不知说什么好。13号要拍毕业照了,没有回去,大学四年,毕竟大家个个圈子之间没什么交流,没什么友谊,果然,整个专业的人只有2/3的人去拍了毕业照。4月23号,拉勾网深圳站,梦想着市集,抱着一点点的小希望,去投了5个,结果一个面试邀请都没有,换之而来的是一片绝望、黑暗。。。。
5月初回学校了赶毕业设计,回学校也是,天天往图书馆跑,为了面试,毕业设计拉下太多了。悠闲的时候,总会想起我喜欢的那个人,过的怎么样了,微信一开,好像自己被屏蔽了还是被删了?这三个月来的面试打击,真的不知道怎么应对,我更不知道怎么跟她聊天,以至于慢慢隔离,这段感情,或许只有我知道自己有多心疼,只有我自己sb得一点点的呵护着。想起上次跟她聊天是4月中,还是她提了个问题其他什么都没问,彼此之前已经没有关心了。删了吧删了吧,就这样一句再见都没有说,一句“祝福”都没有说。 5月底遇到两次还算开心的事,导师问我要不要评优秀论文,不是很想折腾,拒了,导师仍旧劝我,只是写个3页的小总结而已,评上的话能留图书馆收藏。另一个件是珍爱网打电话过来说我一面过了,什么时候二面,天啊,3月份一面,5月底才通知,心里想骂,但是真的好开心好开心,黑暗中的光芒,泪奔。。。。约到了6月15号面试。 6月初开始自己的回家倒计时,请那唯一一个跟我好的妹纸吃饭,忙完答辩,然后是优秀论文的小报告,13号下午交了上去,再回到宿舍,空无一人,一遍忍着不要悲伤,一遍收拾东西,然后向每个宿舍敲门,告别,一个人拖着行李箱,嗯,我要淡定。
准备了有点久,我以为自己准备好了,15号,深圳,珍爱网,被问到:有没有JVM调优经验?ZK是如何保持一致性的?有没有写过ELK索引经验?谈谈你对dubbo的理解。。。事后,得知自己没有过。心态真的炸了,通宵打了两天游戏。月底得知,优秀论文也没有被评上,已经没有骂人吐槽的想法,只是哦的一声,6月,宣告了自己春招结束,宣告自己要去外包,同时也宣告自己大学这些努力全部白费,跟那个灌毒鸡汤天天打游戏的人同一起跑线。
七月初入职的时候,问了问那些一起应届入职的同学,跟别人交流一下面试被问的问题,只有线程池,JVM回收算法,他们也没用过,我无奈,为什么我被问到的,全是Angularjs、MD5加密算法、ZK一致性原理这种奇葩的问题。。。。。
',16),m={href:"http://www.wenzhihuai.com",target:"_blank",rel:"noopener noreferrer"},_=t('决心先好好呆着,学习学习,把公司的产品业务技术专研专研,7、8月份是挺开心的,没怎么加班,虽然下班我都留在公司学习,虽然公司的产品不咋地,但是还是有可学之处的,随后,我没想到的是,公司要实行996,,,9月份至12月,每天都是改bug,开发模块,写业务,全都是增删改查,没有涉及redis、rabbitmq,就真的只有使用Hibernate、spring,而做出的东西,要使用JDK6来运行,至今都能看到sun的类。每天的状态就是累累累,我们组还不是最惨的,隔壁项目组,一周通宵两次,9月到10都是如此,国庆加班,每次经过他们组,我都觉得自己还算是幸福。很厌恶这种不把员工当人看的行为,有同事调侃,明年估计我们这些应届过来的,是不是要走掉2/3的人?疯狂加班的代价是牺牲积极性,我问过那些一周通宵两次的人,敲代码,程序员你喜欢这份工作么?不喜欢!!!有那么一两次,实在是太累了,我都感觉自己有点不行了,想离职,可是,会有公司要我么?会有公司不问那些奇葩的问题么?
双十一,花了500多买了十本书,神书啊,计算机届的圣经啊,然而至今一本都没看完,甚至是都没怎么翻,每天下班回去之后就是9点半了,烧水,洗澡,洗衣服,近11点了,躺床上,一遍看书一遍看鹌鹑直播,一小时才翻几页,996真的太累了,实在看不下书。经过这几个月的加班,感觉自己技术增长为负数,也病了几次,同学见我,都感觉我老了不少,心里不平,可是又没有什么办法。
珠江新城,一年四季都那么美,今晚应该又是万人一起倒计时了吧?已经好久没跟别人一起出去玩过了。今年貌似得了自闭症?社交全靠黄段子。想找人出门走走,然而手机已经没有那种几年的朋友,大学同学一个都不在,哈哈哈,别人说社交软件让人离得更近,我怎么发现我自己一个好朋友都没有了呢,哭笑ing。或许是自己的确不善于跟别人保持联系吧,这个要改,一定要改!!! 远看:
近看:
题外话: 有些时候,遇到不顺,感觉自己不是神,没必要忍住,就开始宣泄自己的情绪,比如疯狂打游戏,只能这样度过了吧。
今年听过最多的一句话,你好厉害,你好牛逼==我感觉也是,可惜,要被业务耽误了不少时间吧。。
《企业IT架构转型之道》是今年最好的架构图书,没有之一。
想起初中,老师让我在黑板写自己的英文作文,好像是信件,描述什么状况来这?最后夸了夸我在文章后面加的一段大概是劝别人不要悲伤要灿烂。也想起一个故事,二战以后,德国满目疮痍,他们处在这样一个凄惨的境地,还能想到在桌上摆设一些花,就一定能在废墟上重建家园。我所看到的是即使在再黑暗的状态下,要永远抱着希望。明年要更加努力的学习,而不是工作,是该挽留一些人了,多运动。说了那么多,其实,我只希望自己的运气不要再差了,再差就没法活了。人在遇到不顺的时候,往往最难过的一关便是自己。哈哈哈,只能学我那个同学给自己灌毒鸡汤了。加油加油↖(^ω^)↗
',13);function b(w,x){const e=o("ExternalLinkIcon");return g(),h("div",null,[c,s,d,l,a("p",null,[i("春节期间,把自己的"),a("a",u,[i("网站"),n(e)]),i("整理了一下,看了看JVM和并发的知识,偶尔刷刷牛客网的题,觉得自己没多大问题了,跟同学出发,去深圳,我并没有一个做马化腾、马云的梦,但是我想,要做出点东西,至少,不是个平庸的人。投了几个,第一个面试,知道是培训兼外包后果断弃了,春招还没开始,再多准备准备,有时间就往深圳图书馆里跑,找个位置,静静的复习,那会,感觉自己一定能去个大公司吧,搞搞分布式、大数据之类的~")]),f,a("p",null,[i("没有环境,就自己创造,下班有时间就改改自己的网站"),a("a",m,[i("http://www.wenzhihuai.com"),n(e)]),i(",GitHub目前有154个star了,感觉也是个小作品吧,把常用的Java技术栈(RabbitMQ、MongoDB等)加了上去,虽然没啥使用场景,但是自己好好努力吧,毕竟高并发、高可用是自己的梦想。今年GitHub自己的提交次数。")]),_])}const y=r(p,[["render",b],["__file","2017.html.vue"]]);export{y as default}; diff --git a/assets/2017.html-cyR93hTD.js b/assets/2017.html-cyR93hTD.js new file mode 100644 index 00000000..eda5d83b --- /dev/null +++ b/assets/2017.html-cyR93hTD.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-22e4234a","path":"/life/2017.html","title":"2017","lang":"zh-CN","frontmatter":{"description":"2017 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/life/2017.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2017"}],["meta",{"property":"og:description","content":"2017 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:53:47.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:53:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2017\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T04:53:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706244827000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":11.22,"words":3366},"filePathRelative":"life/2017.md","localizedDate":"2024年1月24日","excerpt":"2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
\\n17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?
and ... 广州拜拜,晚上人少的时候,跟妹纸去珠江新城真的很美好
入职的时候是做日志收集的,就是flume+kafka+es这一套,遇到了不少大佬,嗯,感觉挺好的,打算好好干,一个半月后不知怎么滴,被拉去做容器云了,然后就开始了天天调jenkins、gitlab这些没什么用的api,开始了增删改查的历程,很烦,研究jenkins、gitlab对于自己的技术没什么提升,又不得不做,而且大小周+加班这么做,回到家自己看java、netty、golang的书籍,可是没项目做没实践,过几个月就忘光了,有段时间真的很烦很烦,根本不想工作,天天调api,而且是jenkins、gitlab这些没什么卵用的api,至今也没找到什么办法。。。。 公司发展太快,也实在让人不知所措,一不小心部门被架空了,什么预兆都没有,被分到其他部门,然后发现这个部门领导内斗真的是,“三国争霸”吧,无奈成为炮灰,不同的领导不同的要求,安安静静敲个代码怎么这么难。。。。 去年的学习拉下了不少,文章也写的少了,总写那些入门的文章确实没有什么意思,买的书虽然没有17年的多吧,不过也不少了,也看了挺多本书的,虽然我还是不满意自己的阅读量。 这几年要开始抓准一个框架了,好好专研一下,也要开始学习一下go了,希望能参与一些github的项目,好好努力吧。
平时没事总是宅在家里打游戏,差不多持续到下半年吧,那会打游戏去楼下吃东西的时候老是觉得眼神恍惚,盯着电子产品太多了,然后就跟别人跑步去了,每周去一次人才公园,跑步健身,人才公园真的是深圳跑步最好的一个地方了,桥上总有一群摄影师,好想在深圳湾买一套房子,看了看房价,算了算了,深圳的房,真的贵的离谱。19年也要多多运动,健康第一健康第一。
想不起来还有什么要说的了,毕竟程序员,好像每天的生活都一样,就展望一下19年吧。今年,最最重要的是家里人以及身边还有所有人都健健康康的,哈哈哈。然后是安安静静的敲代码~~就酱
',13);function u(m,b){const i=r("ExternalLinkIcon");return o(),h("div",null,[l,d,e("p",null,[a("想着17年12月31号写的那篇文章"),e("a",p,[a("https://www.cnblogs.com/w1570631036/p/8158284.html"),n(i)]),a(",感叹18年还算恢复了点。")]),g,e("p",null,[a("其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。 去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,"),e("a",f,[a("面经在这"),n(i)]),a(",之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。")]),_])}const k=t(s,[["render",u],["__file","2018.html.vue"]]);export{k as default}; diff --git a/assets/2018.html-pd4XfNXB.js b/assets/2018.html-pd4XfNXB.js new file mode 100644 index 00000000..87f5292d --- /dev/null +++ b/assets/2018.html-pd4XfNXB.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1f7a720c","path":"/life/2018.html","title":"2018","lang":"zh-CN","frontmatter":{"description":"2018 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。 想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。 心惊胆战的裸辞经历","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/life/2018.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2018"}],["meta",{"property":"og:description","content":"2018 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。 想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。 心惊胆战的裸辞经历"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:53:47.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:53:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2018\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T04:53:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"心惊胆战的裸辞经历","slug":"心惊胆战的裸辞经历","link":"#心惊胆战的裸辞经历","children":[]},{"level":2,"title":"工作工作","slug":"工作工作","link":"#工作工作","children":[]},{"level":2,"title":"跑步跑步","slug":"跑步跑步","link":"#跑步跑步","children":[]},{"level":2,"title":"Others","slug":"others","link":"#others","children":[]},{"level":2,"title":"2019","slug":"_2019","link":"#_2019","children":[]}],"git":{"createdTime":1706066758000,"updatedTime":1706244827000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":4.35,"words":1306},"filePathRelative":"life/2018.md","localizedDate":"2024年1月24日","excerpt":"年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。
\\n想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。
\\n2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。\\n19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。
\\n从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。
","autoDesc":true}');export{e as data}; diff --git a/assets/2019.html-ogBQCkli.js b/assets/2019.html-ogBQCkli.js new file mode 100644 index 00000000..583dec10 --- /dev/null +++ b/assets/2019.html-ogBQCkli.js @@ -0,0 +1 @@ +import{_ as a}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as e,c as i,e as n}from"./app-cS6i7hsH.js";const r={},t=n('2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。
从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。
之后有了点起色,然后开始着重强调了DevOps的重要性,期初也没人理解,还被骂天天研究技术不干活。。。这东西没技术,但是真的是公司十分需要的公司好么!!可惜公司对这个产品很失望,也不给人,期间还说要暂停项目,唉,这么大的项目就给几个人怎么做,谁都不乐意,就这么过的很憋屈吧,开始了维护的日子。。。
工作上,蛮多人对我挺好的,领导,同事,不过有时候憋屈的时候还是有了点情绪化,有时候有点悲伤的想法,浮躁、暴躁,出去面试了几次,感觉不是自己能力不好,而是市场上的真的需要3+年以上的。。。。不过感觉自己能在这里憋屈的过了这么久,也是很佩服自己的,哈哈,用同事的话来说就是:当做游戏里的练级,锻炼自己哈哈哈
项目的方向错误,导致自己写的代码都是很简单的增删改查,没有技术含量的那种,也最终导致自己浪费了不少时间,上半年算是很气愤吧,不想就这么浪费掉这些时间,虽然期间看了各种各样的东西,比如Netty、Go、操作系统什么的,最终发现,如果不在项目中使用的话,真的会忘,而且很快就忘掉,最后的还是决定学习项目相关度比较大的东西了,比如Go,如果项目不是很大,小项目的话,就用Go来写,这么做感觉提升还是挺大的。 过去一年往南山图书馆借了不少书,省了不少钱,虽然没怎么看,但我至少有个奋斗的心啊,哈哈哈哈哈哈哈哈,文章写得少了,因为感觉写不出好东西,总是入门的那种,不深,不值得学习,想想到时候把别人给坑了,也不好意思。 操作系统感觉还是要好好学学了,加油
Kubernetes就是未来的方向,有时候翻开源码看看,又不敢往下面看了,对底层不熟吧,今年要多多研究下Kubernetes的源码了,至于Spring那一套,也不知道该不该放弃,或者都学习一下?云原生就是趋势。
吵架了,没心思写
过去一年还是把运动算是坚持了下来,每个月必去深圳湾跑一次。还是没怎么睡好,工作感情上都有点不顺,加上自己本身就难以入睡,有时候躺床上就是怎么也睡不着,还长了痘痘,要跑去医院那种,可怕,老了,还是要早点睡觉,多走走多运动,好久没打羽毛球了,自己也不想有空的时候只会打游戏,今年继续加油,多运动吧。
还爬了两次南山
看了周围蛮多同事去了腾讯阿里,有点心动,好想去csig,没到3年真的让人很抓狂。 过去一年过的蛮憋屈的,特别是工作不顺,加上跟女朋友吵架,心态爆炸。。。
每年这个时候,都不敢有太大的期望了,祝大家都健健康康的,工作顺利!!
当然,如果有可能的话,我想去CSIG
',23),h=[t];function d(c,p){return e(),i("div",null,h)}const g=a(r,[["render",d],["__file","2019.html.vue"]]);export{g as default}; diff --git "a/assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-73kuTv_z.js" "b/assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-73kuTv_z.js" new file mode 100644 index 00000000..d6464a02 --- /dev/null +++ "b/assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-73kuTv_z.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-731a76b6","path":"/personalWebsite/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html","title":"3.定时任务","lang":"zh-CN","frontmatter":{"description":"3.定时任务 先看一下Quartz的架构图: 一.特点: 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/personalWebsite/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"3.定时任务"}],["meta",{"property":"og:description","content":"3.定时任务 先看一下Quartz的架构图: 一.特点: 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"3.定时任务\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"一.特点:","slug":"一-特点","link":"#一-特点","children":[]},{"level":3,"title":"二.主要组成部分","slug":"二-主要组成部分","link":"#二-主要组成部分","children":[]},{"level":3,"title":"三、Quartz设计","slug":"三、quartz设计","link":"#三、quartz设计","children":[]},{"level":3,"title":"四、使用","slug":"四、使用","link":"#四、使用","children":[]},{"level":2,"title":"Spring的高级特性之定时任务","slug":"spring的高级特性之定时任务","link":"#spring的高级特性之定时任务","children":[]}],"git":{"createdTime":1706182936000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":3.55,"words":1064},"filePathRelative":"personalWebsite/3.定时任务.md","localizedDate":"2024年1月25日","excerpt":"先看一下Quartz的架构图:
\\n\\n先看一下Quartz的架构图:
<dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ <version>4.2.6.RELEASE</version>
+</dependency>
+
(2)导入quartz包
<dependency>
+ <groupId>org.quartz-scheduler</groupId>
+ <artifactId>quartz</artifactId>
+ <version>2.3.0</version>
+</dependency>
+
(3)mysql远程备份 使用命令行工具仅仅需要一行:
mysqldump -u [username] -p[password] -h [hostip] database > file
+
但是java不能直接执行linux的命令,仍旧需要依赖第三方库ganymed
<dependency>
+ <groupId>ch.ethz.ganymed</groupId>
+ <artifactId>ganymed-ssh2</artifactId>
+ <version>262</version>
+</dependency>
+
完整代码如下:
@Component("mysqlService")//在spring中注册一个mysqlService的Bean
+public class MysqlUtil {
+ ...
+ StringBuffer sb = new StringBuffer();
+ sb.append("mysqldump -u " + username + " -p" + password + " -h " + host + " " +
+ database + " >" + file);
+ String sql = sb.toString();
+ Connection connection = new Connection(s_host);
+ connection.connect();
+ boolean isAuth = connection.authenticateWithPassword(s_username, s_password);//进行远程服务器登陆认证
+ if (!isAuth) {
+ logger.error("server login error");
+ }
+ Session session = connection.openSession();
+ session.execCommand(sql);//执行linux语句
+ InputStream stdout = new StreamGobbler(session.getStdout());
+ BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
+ ...
+}
+
(4)spring中配置quartz
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
+ <property name="targetObject" ref="mysqlService"/>
+ <property name="targetMethod" value="exportDataBase"/>
+</bean>
+<!--定义触发时间 -->
+<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
+ <property name="jobDetail" ref="jobDetail"/>
+ <!-- cron表达式,每周五2点59分运行-->
+ <property name="cronExpression" value="0 59 2 ? * FRI"/>
+</bean>
+<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->
+<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
+ <property name="triggers">
+ <list>
+ <ref bean="myTrigger"/>
+ </list>
+ </property>
+</bean>
+
java ee项目的定时任务中除了运行quartz之外,spring3+还提供了task,可以看做是一个轻量级的Quartz,而且使用起来比Quartz简单的多。
(1)spring配置文件中配置:
<task:annotation-driven/>
+
(2)最简单的例子,在所需要的函数上添加定时任务即可运行
@Scheduled(fixedRate = 5000)
+ public void reportCurrentTime() {
+ System.out.println("每隔5秒运行一次" + sdf.format(new Date()));
+ }
+
(3)运行的时候会报错:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
+ at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
+ at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
+ at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
+ at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:163)
+ at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:136)
+ at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:380)
+
1.尝试从配置中找到一个TaskScheduler Bean
2.寻找ScheduledExecutorService Bean
3.使用默认的scheduler 修改log4j.properties即可: log4j.logger.org.springframework.scheduling=INFO
其实这个功能不影响定时器的功能。 (4)结果:
每隔5秒运行一次14:44:34
+每隔5秒运行一次14:44:39
+每隔5秒运行一次14:44:44
+
先在百度统计中注册登录之后,进入管理页面,新增网站,然后在代码管理中获取安装代码,大部分人的代码都是类似的,除了hm.js?后面的参数,是记录该网站的唯一标识。
<script>
+var _hmt = _hmt || [];
+(function() {
+ var hm = document.createElement("script");
+ hm.src = "https://hm.baidu.com/hm.js?code";
+ var s = document.getElementsByTagName("script")[0];
+ s.parentNode.insertBefore(hm, s);
+})();
+</script>
+
同时,需要在申请其他设置->数据导出服务中开通数据导出服务,百度统计Tongji API可以为网站接入者提供便捷的获取网站流量数据的通道。
至此,我们获得了username、password、token,然后开始使用三个参数来获取数据。
参数名称 | 参数类型 | 描述 |
---|---|---|
method | string | 要查询的报告 |
start_date | string | 查询起始时间 |
end_date | string | 查询结束时间 |
metrics | string | 自定义指标 |
其中,参数start_date和end_date的规定为:yyyyMMdd,这里我们使用python的原生库,datetime、time,获取昨天的时间以及前七天的日期。
today = datetime.date.today() # 获取今天的日期
+yesterday = today - datetime.timedelta(days=1) # 获取昨天的日期
+fifteenago = today - datetime.timedelta(days=7) # 获取前七天的日期
+end, start = str(yesterday).replace("-", ""), str(fifteenago).replace("-", "") # 格式化成yyyyMMdd格式
+
说明:siteId可以根据个人百度统计的链接获取,也可以使用Tongji API的第一个接口列表获取用户的站点列表。首先,我们构建一个类,由于username、password、token都是通用的,所以我们将它设置为构造方法的参数。
class Baidu(object):
+ def __init__(self, siteId, username, password, token):
+ self.siteId = siteId
+ self.username = username
+ self.password = password
+ self.token = token
+
然后构建一个共同的方法,用来获取提交数据之后返回的结果,其中提供了4个可变参数,分别是(start_date:起始日期,end_date:结束日期,method:方法,metrics:指标),返回的是字节,最后需要decode("utf-8")一下变成字符:
def getresult(self, start_date, end_date, method, metrics):
+ body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+ "username": self.username},
+ "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+ "end_date": end_date,
+ "metrics": metrics}}
+ data = bytes(json.dumps(body), 'utf8')
+ req = urllib.request.Request(base_url, data)
+ response = urllib.request.urlopen(req)
+ the_page = response.read()
+ return the_page.decode("utf-8")
+
def getresult(self, start_date, end_date, method, metrics, **kw):
+ base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+ body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+ "username": self.username},
+ "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+ "end_date": end_date, "metrics": metrics}}
+ for key in kw: #对可变参数进行遍历,如果有的话就往body中加入
+ body['body'][key] = kw[key]
+
使用方式:
result = self.getresult(start, end, "source/all/a",
+ "pv_count,visitor_count,avg_visit_time", viewType='visitor') #其中viewTYpe便是可变参数
+
(2)获取的数据如何解析 百度统计返回的结果比较简洁而又反人类,以获取概览中的pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time为例子:
result = bd.getresult(start, end, "overview/getTimeTrendRpt",
+ "pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time")
+
返回的结果是:
[[['2017/09/12'], ['2017/09/13'], ['2017/09/14'], ['2017/09/15'], ['2017/09/16'], ['2017/09/17'], ['2017/09/18']],
+[[422, 76, 76, 41.94, 221],
+ [284, 67, 65, 50.63, 215],
+ [67, 23, 22, 52.17, 153],
+ [104, 13, 13, 36.36, 243],
+ [13, 4, 4, 33.33, 66],
+ [73, 7, 6, 37.5, 652],
+ [63, 11, 11, 33.33, 385]
+ ], [], []]
+
即:翻译成人话就是:
[[[date1,date2,...]],
+ [[date1的pv_count, date1的visitor_count, date1的ip_count, date1的bounce_ratio, date1的avg_visit_time],
+ [date2的pv_count, date2的visitor_count, date2的ip_count, date2的bounce_ratio, date2的avg_visit_time],
+ ...,[]
+ ],[],[]]
+
极其反人类的设计。还好用的python,python数组的特性实在太强了。出了可以运用[x for x in range]这类语法之外,还能与三元符(x if y else x+1,如果y成立,那么结果是x,如果y不成立,那么结果是x+1)一起使用,这里注意:如果当天访问量为0,其返回的json结果是'--',所以要判断是不是为'--',归0化,才能在折线图等各种图上显示。下面是pv_count的例子:
pv_count = [x[0] if x[0] != '--' else 0 for x in result[1]]
+
(3)每周限制2000次 在开通数据导出服务的时候,不知道大家有没有注意到它的说明,即我们是不能实时监控的,只能将它放在临时数据库中,这里我们选择了Redis,并在centos里定义一个定时任务,每天全部更新一次即可。
python中redis的使用方法很简单,连接跟mysql类似:
# 字符串
+pool = redis.ConnectionPool(host='your host ip', port=port, password='your auth') # TODO redis地址
+r = redis.Redis(connection_pool=pool)
+
本网站使用redis的数据结构只有set,方法也很简单,就是定义一个key,然后value是数组的字符串获取json。
ip_count = [x[2] if x[2] != '--' else 0 for x in result[1]]
+r.set("ip_count", ip_count)
+# json
+name = [item[0]['name'] for item in data[0]]
+count = 0
+tojson = []
+for item in data[1]:
+ temp = {}
+ temp["name"] = name[count]
+ temp["pv_count"] = item[0]
+ temp["visitor_count"] = item[1]
+ temp["average_stay_time"] = item[2]
+ tojson.append(temp)
+ count = count + 1
+r.set("rukouyemian", json.dumps(tojson[:5]))
+
import json
+import time
+import datetime
+import urllib.parse
+import urllib.request
+
+base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+
+class Baidu(object):
+ def __init__(self, siteId, username, password, token):
+ self.siteId = siteId
+ self.username = username
+ self.password = password
+ self.token = token
+
+ def getresult(self, start_date, end_date, method, metrics, **kw):
+ base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+ body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+ "username": self.username},
+ "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+ "end_date": end_date, "metrics": metrics}}
+ for key in kw:
+ body['body'][key] = kw[key]
+ data = bytes(json.dumps(body), 'utf8')
+ req = urllib.request.Request(base_url, data)
+ response = urllib.request.urlopen(req)
+ the_page = response.read()
+ return the_page.decode("utf-8")
+
+if __name__ == '__main__':
+ # 日期开始
+ today = datetime.date.today()
+ yesterday = today - datetime.timedelta(days=1)
+ fifteenago = today - datetime.timedelta(days=7)
+ end, start = str(yesterday).replace("-", ""), str(fifteenago).replace("-", "")
+ # 日期结束
+ bd = Baidu(yoursiteid, "username", "password", "token")
+ result = bd.getresult(start, end, "overview/getTimeTrendRpt",
+ "pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time")
+ result = json.loads(result)
+ base = result["body"]["data"][0]["result"]["items"]
+ print(base)
+
+
String pv_count = jedis.get("pv_count");
+String visitor_count = jedis.get("visitor_count");
+mv.addObject("pv_count", pv_count);
+mv.addObject("visitor_count", visitor_count);
+
jsp中的使用如下:
<div class="panel-heading" style="background-color: rgba(187,255,255,0.7)">
+ <div class="card-title">
+ <strong>PV和UV折线图</strong>
+
+
+ <div class="panel-body">
+ <div id="linecontainer" style="width: auto;height: 330px">
+ <script>
+ var chart = new Highcharts.Chart('linecontainer', {
+ title: {
+ text: null
+ },
+ credits: {
+ enabled: false
+ },
+ xAxis: {
+ categories: ${daterange}
+ },
+ yAxis: {
+ title: {
+ text: '次数'
+ },
+ plotLines: [{
+ value: 0,
+ width: 1,
+ color: '#808080'
+ }]
+ },
+ tooltip: {
+ valueSuffix: '次'
+ },
+ legend: {
+ borderWidth: 0,
+ align: "center", //程度标的目标地位
+ verticalAlign: "top", //垂直标的目标地位
+ x: 0, //间隔x轴的间隔
+ y: 0 //间隔Y轴的间隔
+ },
+ series: [{
+ name: 'pv',
+ data:${pv_count}
+ }, {
+ name: 'uv',
+ data:${visitor_count}
+ }]
+ })
+ </script>
+
效果如下:
(2)地域访问量 在python代码中先获取地域的数据,其结果如下,百度统计跟echarts都是百度的,果然,自家人对自己人的支持真是特别友好的。
[{'pv_count': 649, 'pv_ratio': 7, 'visitor_count': 2684, 'name': '广东'}, {'pv_count': 2, 'pv_ratio': 2, 'visitor_count': 76, 'name': '四川'}, {'pv_count': 1, 'pv_ratio': 1, 'visitor_count': 3, 'name': '江苏'}]
+
<script type="text/javascript">
+ var myChart = echarts.init(document.getElementById('diyu'));
+ option = {
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ visualMap: {
+ min: 0,
+ max:\${diyumax},
+ left: 'left',
+ top: 'bottom',
+ text: ['高', '低'], // 文本,默认为数值文本
+ calculable: true
+ },
+ toolbox: {
+ show: true,
+ orient: 'vertical',
+ left: 'right',
+ top: 'center',
+ feature: {
+ dataView: {readOnly: false},
+ restore: {},
+ saveAsImage: {}
+ }
+ },
+ series: [
+ {
+ name: '访问量',
+ type: 'map',
+ mapType: 'china',
+ roam: false,
+ label: {
+ normal: {
+ show: true
+ },
+ emphasis: {
+ show: true
+ }
+ },
+ data: [
+ <c:forEach var="diyu" items="\${diyu}">
+ {name: '\${diyu.name}', value: \${to.pv_count}},
+ </c:forEach>
+ ]
+ }
+ ]
+ };
+ myChart.setOption(option);
+</script>
+
结果如下:
欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
","autoDesc":true}');export{e as data}; diff --git a/assets/404.html-WhIsgby4.js b/assets/404.html-WhIsgby4.js new file mode 100644 index 00000000..ffb11d95 --- /dev/null +++ b/assets/404.html-WhIsgby4.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-3706649a","path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound","description":"","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/404.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"article:author","content":"Zephery"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null,"excerpt":"","autoDesc":true}');export{t as data}; diff --git a/assets/404.html-tbxpD8LH.js b/assets/404.html-tbxpD8LH.js new file mode 100644 index 00000000..63b92ac1 --- /dev/null +++ b/assets/404.html-tbxpD8LH.js @@ -0,0 +1 @@ +import{_ as e}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as t,c}from"./app-cS6i7hsH.js";const o={};function r(_,n){return t(),c("div")}const f=e(o,[["render",r],["__file","404.html.vue"]]);export{f as default}; diff --git "a/assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-ZTiwy8Ji.js" "b/assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-ZTiwy8Ji.js" new file mode 100644 index 00000000..d37837be --- /dev/null +++ "b/assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-ZTiwy8Ji.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3efc517e","path":"/personalWebsite/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html","title":"5.小集群部署.md","lang":"zh-CN","frontmatter":{"description":"5.小集群部署.md 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/personalWebsite/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"5.小集群部署.md"}],["meta",{"property":"og:description","content":"5.小集群部署.md 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"5.小集群部署.md\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1.1 Nginx的安装","slug":"_1-1-nginx的安装","link":"#_1-1-nginx的安装","children":[]},{"level":2,"title":"1.2 Nginx的配置","slug":"_1-2-nginx的配置","link":"#_1-2-nginx的配置","children":[]},{"level":2,"title":"测试:","slug":"测试","link":"#测试","children":[]}],"git":{"createdTime":1706182936000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":9.5,"words":2849},"filePathRelative":"personalWebsite/5.小集群部署.md","localizedDate":"2024年1月25日","excerpt":"欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。
\\n洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。
启动后页面如下:
记一下常用命令
启动nginx,由于是采用rpm方式,所以环境变量什么的都配置好了。
+[root@beijingali ~]# nginx #启动nginx
+[root@beijingali ~]# nginx -s reload #重启nginx
+[root@beijingali ~]# nginx -t #校验nginx配置文件
+nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
+nginx: configuration file /etc/nginx/nginx.conf test is successful
+
http{
+ ...
+ upstream backend {
+ hash $uri;
+ # 北京节点
+ server 47.95.10.139:8080;
+ # 深圳节点
+ server 119.23.46.71:8080;
+ }
+
+ server {
+ ...
+ location / {
+ root html;
+ index index.html index.htm;
+ proxy_pass http://backend;
+ ...
+ }
+ ...
+
之前有使用过ELK来跟踪日志,所以将日志格式化成了json的格式,这里贴一下吧
...
+ log_format main '{"@timestamp":"$time_iso8601",'
+ '"host":"$server_addr",'
+ '"clientip":"$remote_addr",'
+ '"size":$body_bytes_sent,'
+ '"responsetime":$request_time,'
+ '"upstreamtime":"$upstream_response_time",'
+ '"upstreamhost":"$upstream_addr",'
+ '"http_host":"$host",'
+ '"url":"$uri",'
+ '"xff":"$http_x_forwarded_for",'
+ '"referer":"$http_referer",'
+ '"agent":"$http_user_agent",'
+ '"status":"$status"}';
+ access_log logs/access.log main;
+ ...
+
location / {
+ root html;
+ index index.html index.htm;
+ proxy_pass http://backend;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_set_header REMOTE-HOST $remote_addr;
+ }
+
(1)proxy_set_header X-real-ip $remote_addr;
其中这个X-real-ip是一个自定义的变量名,名字可以随意取,这样做完之后,用户的真实ip就被放在X-real-ip这个变量里了,然后,在web端可以这样获取: request.getAttribute("X-real-ip")
(2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
X-Forwarded-For:squid开发的,用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准,这个不是默认有的,其经过代理转发之后,格式为client1, proxy1, proxy2,如果想通过这个变量来获取用户的ip,那么需要和$proxy_add_x_forwarded_for一起使用。
$proxy_add_x_forwarded_for:现在的$proxy_add_x_forwarded_for变量,X-Forwarded-For部分包含的是用户的真实ip,$remote_addr部分的值是上一台nginx的ip地址,于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip,第一台nginx的ip”。
首先需要下载证书,放在nginx.conf相同目录下,nginx上的配置也需要有所改变,在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥:
ssl_certificate 1_www.wenzhihuai.com_bundle.crt; #主要文件路径
+ ssl_certificate_key 2_www.wenzhihuai.com.key;
+ ssl_session_timeout 5m; # 超时时间
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
+ ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
+ ssl_prefer_server_ciphers on;
+
至此,可以使用https来访问了。https带来的安全性(保证信息安全、识别钓鱼网站等)是http远远不能比拟的,目前大部分网站都是实现全站https,还能将http自动重定向为https,此处,需要在server中添加rewrite ^(.*) https://$server_name$1 permanent;即可
配置好了负载均衡之后,如果有一台服务器挂了怎么办?nginx中提供了可配置的服务器存活的识别,主要是通过max_fails失败请求次数,fail_timeout超时时间,weight为权重,下面的配置的意思是当服务器超时10秒,并失败了两次的时候,nginx将认为上游服务器不可用,将会摘掉上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试
upstream backend_server {
+ server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;
+ server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;
+}
+
首先,添加相关依赖
<dependency>
+ <groupId>org.springframework.session</groupId>
+ <artifactId>spring-session-data-redis</artifactId>
+ <version>1.3.1.RELEASE</version>
+ <type>pom</type>
+ </dependency>
+ <dependency>
+ <groupId>redis.clients</groupId>
+ <artifactId>jedis</artifactId>
+ <version>\${jedis.version}</version>
+ </dependency>
+
新建一个session.xml,然后在spring的配置文件中添加该文件,然后在session.xml中添加:
<!-- redis -->
+ <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
+ </bean>
+
+ <bean id="jedisConnectionFactory"
+ class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+ <property name="hostName" value="\${host}" />
+ <property name="port" value="\${port}" />
+ <property name="password" value="\${password}" />
+ <property name="timeout" value="\${timeout}" />
+ <property name="poolConfig" ref="jedisPoolConfig" />
+ <property name="usePool" value="true" />
+ </bean>
+
+ <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+ <property name="connectionFactory" ref="jedisConnectionFactory" />
+ </bean>
+
+ <!-- 将session放入redis -->
+ <bean id="redisHttpSessionConfiguration"
+ class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
+ <property name="maxInactiveIntervalInSeconds" value="1800" />
+ </bean>
+
然后我们需要保证servlet容器(tomcat)针对每一个请求都使用springSessionRepositoryFilter来拦截
<filter>
+ <filter-name>springSessionRepositoryFilter</filter-name>
+ <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+</filter>
+<filter-mapping>
+ <filter-name>springSessionRepositoryFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ <dispatcher>REQUEST</dispatcher>
+ <dispatcher>ERROR</dispatcher>
+</filter-mapping>
+
配置完成,使用RedisDesktopManager查看结果:
访问http://www.wenzhihuai.com
tail -f localhost_access_log.2017-11-05.txt查看日志,然后清空一下当前记录
访问技术杂谈页面,此时nginx将请求转发到119.23.46.71服务器,session为28424f91-5bc5-4bba-99ec-f725401d7318。
点击生活笔记页面,转发到的服务器为47.95.10.139,session为28424f91-5bc5-4bba-99ec-f725401d7318,与上面相同。session已保持一致。
`,15),P=n("strong",null,"值得注意的是:同一个浏览器,在没有关闭的情况下,即使通过域名访问和ip访问得到的session是不同的。",-1),E={href:"http://www.wenzhihuai.com",target:"_blank",rel:"noopener noreferrer"},j=n("br",null,null,-1),L={href:"https://github.com/Zephery/newblog",target:"_blank",rel:"noopener noreferrer"};function R(C,N){const a=i("ExternalLinkIcon");return o(),l("div",null,[u,n("p",null,[s("欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:"),n("a",r,[s("http://www.wenzhihuai.com"),t(a)]),s(",个人网站代码地址:"),n("a",d,[s("https://github.com/Zephery/newblog"),t(a)]),s("。"),g,s(" 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。")]),k,n("p",null,[s("一般情况下,当单实例无法支撑起用户的请求时,就需要就行扩容,部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区,比如:深圳的用户却访问到了北京的节点,然后还得从北京返回处理之后的数据,光是来回就至少得30ms。这部分可以通过智能DNS(就近访问)解决。而分机房,需要将请求合理的分配到不同的服务器,这部分就是我们所需要处理的。 通常,负载均衡分为硬件和软件两种,硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层,LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡,但是暂不讨论(我木有硬件条件)。下图来自"),n("a",m,[s("张开涛"),t(a)]),s("的《亿级流量网站架构核心技术》")]),v,h,b,_,f,n("p",null,[s("可以选择tar.gz、yum、rpm安装等,这里,由于编译、nginx配置比较复杂,要是没有把握还是使用rpm来安装吧,比较简单。从"),n("a",q,[s("https://pkgs.org/download/nginx"),t(a)]),s("可以找到最新的rpm包,然后rpm -ivh 文件,然后在命令行中输入nginx即可启动,可以使用netstat检查一下端口。")]),x,n("p",null,[s("Nginx常用的算法有: (1)round-robin:轮询,nginx默认的算法,从词语上可以看出,轮流访问服务器,也可以通过weight来控制访问次数。 (2)ip_hash:根据访客的ip,一个ip地址对应一个服务器。 (3)hash算法:hash算法常用的方式有根据uri、动态指定的consistent_key两种。 使用hash算法的缺点是当添加服务器的时候,只有少部分的uri能够被重新分配到新的服务器。这里,本站使用的是hash uri的算法,将不同的uri分配到不同的服务器,但是由于是不同的服务器,tomcat中的session是不一致,解决办法是"),n("a",w,[s("tomcat session"),t(a)]),s("的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。")]),y,n("p",null,[s("配置完上流服务器之后,需要配置Http的代理,将请求的端口转发到proxy_pass设定的上流服务器,即当我们访问http://wwww.wenzhihuai.com的时候,请求会被转发到backend中配置的服务器,此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是,仔细注意之后,我们会发现,tomcat中的访问日志ip来源都是127.0.0.1,相当于本地访问自己的资源。由于后台中有处理ip的代码,对客户端的ip、访问uri等记录下来,所以需要设置nginx来获取用户的实际ip,参考"),n("a",S,[s("nginx 配置"),t(a)]),s("。参考文中的一句话:经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,通过$remote_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的,也就是说nginx使用$remote_addr变量时获得的是用户的真实ip,如果我们想要在web端获得用户的真实ip,就必须在nginx这里作一个赋值操作,如下:")]),$,n("p",null,[n("a",z,[s("HTTPS"),t(a)]),s("(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。一般情况下,能通过服务器的ssh来生成ssl证书,但是如果使用是自己的,一般浏览器(谷歌、360等)都会报证书不安全的错误,正常用户都不敢访问吧==,所以现在使用的是腾讯跟别的机构颁发的:")]),T,n("p",null,[s("分布式情况下难免会要解决session共享的问题,目前推荐的方法基本上都是使用redis,网上查找的方法目前流行的有下面四种,参考自"),n("a",H,[s("tomcat 集群中 session 共"),t(a)]),s(": 1.使用 filter 方法存储。(推荐,因为它的服务器使用范围比较多,不仅限于tomcat ,而且实现的原理比较简单容易控制。) 2.使用 tomcat sessionmanager 方法存储。(直接配置即可) 3.使用 terracotta 服务器共享。(不知道,不了解) 4.使用spring-session。(spring的一个小项目,其原理也和第一种基本一致)")]),n("p",null,[s("本站使用spring-session,毕竟是spring下的子项目,学习下还是挺好的。参考"),n("a",F,[s("Spring-Session官网"),t(a)]),s("。官方文档提供了spring-boot、spring等例子,可以参考参考。目前最新版本是2.0.0,不同版本使用方式不同,建议看官网的文档吧。")]),I,n("p",null,[P,s(" 欢迎访问我的个人网站O(∩_∩)O哈哈~希望能给个star 个人网站网址:"),n("a",E,[s("http://www.wenzhihuai.com"),t(a)]),j,s(" 个人网站代码地址:"),n("a",L,[s("https://github.com/Zephery/newblog"),t(a)])])])}const B=p(c,[["render",R],["__file","5.小集群部署.html.vue"]]);export{B as default}; diff --git "a/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-6hO0TA2i.js" "b/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-6hO0TA2i.js" new file mode 100644 index 00000000..267c7825 --- /dev/null +++ "b/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-6hO0TA2i.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-79ad699f","path":"/personalWebsite/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html","title":"6.数据库备份","lang":"zh-CN","frontmatter":{"description":"6.数据库备份 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地: 由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/personalWebsite/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"6.数据库备份"}],["meta",{"property":"og:description","content":"6.数据库备份 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地: 由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"6.数据库备份\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"2.1 创建所用的复制账号","slug":"_2-1-创建所用的复制账号","link":"#_2-1-创建所用的复制账号","children":[]},{"level":3,"title":"2.2 配置master","slug":"_2-2-配置master","link":"#_2-2-配置master","children":[]},{"level":3,"title":"2.3 配置slave","slug":"_2-3-配置slave","link":"#_2-3-配置slave","children":[]}],"git":{"createdTime":1706182936000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":5.77,"words":1732},"filePathRelative":"personalWebsite/6.数据库备份.md","localizedDate":"2024年1月25日","excerpt":"先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:
\\n\\n由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:
","autoDesc":true}');export{e as data}; diff --git "a/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-GrbFXSWT.js" "b/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-GrbFXSWT.js" new file mode 100644 index 00000000..16387227 --- /dev/null +++ "b/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-GrbFXSWT.js" @@ -0,0 +1,65 @@ +import{_ as s}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as l,o as r,c as d,a as i,d as e,b as a,e as t}from"./app-cS6i7hsH.js";const m={},c=t(`先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:
由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:
果然,36ms。。。看起来挺小的,但是对比一下sql执行语句的时间:
大部分都能在10ms内完成,而最长的语句是insert语句,可见,由于异地导致的36ms延时还是比较大的,捣鼓了一下,最后还是选择换个架构,每个服务器读取自己的数据库,然后数据库底层做一下主主复制,让数据同步。最终架构如下:
数据库复制的基本问题就是让一台服务器的数据与其他服务器保持同步。MySql目前支持两种复制方式:基于行的复制和基于语句的复制,这两者的基本过程都是在主库上记录二进制的日志、在备库上重放日志的方式来实现异步的数据复制。其过程分为三步:
(1)master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
(2)slave将master的binary log events拷贝到它的中继日志(relay log);
(3)slave重做中继日志中的事件,将改变反映它自己的数据。
该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。 下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。 SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。 此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。 MySql的基本复制方式有主从复制、主主复制,主主复制即把主从复制的配置倒过来再配置一遍即可,下面的配置则是主从复制的过程,到时候可自行改为主主复制。其他的架构如:一主库多备库、环形复制、树或者金字塔型都是基于这两种方式,可参考《高性能MySql》。
由于是个自己的小网站,就不做过多的操作了,直接使用root账号
接下来要对mysql的serverID,日志位置,复制方式等进行操作,使用vim打开my.cnf。
[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# master
+log-bin=mysql-bin
+# 设为基于行的复制
+binlog-format=ROW
+# 设置server的唯一id
+server-id=2
+# 忽略的数据库,不使用备份
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 要进行备份的数据库
+binlog-do-db=myblog
+
重启Mysql之后,查看主库状态,show master status。
其中,File为日志文件,指定Slave从哪个日志文件开始读复制数据,Position为偏移,从哪个POSITION号开始读,Binlog_Do_DB为要备份的数据库。
从库的配置跟主库类似,vim /etc/my.cnf配置从库信息。
+[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# slave
+log-bin=mysql-bin
+# 服务器唯一id
+server-id=3
+# 不备份的数据库
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 需要备份的数据库
+replicate-do-db=myblog
+# 其他相关信息
+slave-skip-errors=all
+slave-net-timeout=60
+# 开启中继日志
+relay_log = mysql-relay-bin
+#
+log_slave_updates = 1
+# 防止改变数据
+read_only = 1
+
重启slave,同时启动复制,还需要调整一下命令。
mysql> CHANGE MASTER TO MASTER_HOST = '119.23.46.71', MASTER_USER = 'root', MASTER_PASSWORD = 'helloroot', MASTER_PORT = 3306, MASTER_LOG_FILE = 'mysql-bin.000009', MASTER_LOG_POS = 346180;
+
+
可以看见slave已经开始进行同步了。我们使用show slave status\\G来查看slave的状态。
其中日志文件和POSITION不一致是合理的,配置好了的话,即使重启,也不会影响到主从复制的配置。
某天在Github上漂游,发现了阿里的canal,同时才知道上面这个业务是叫异地跨机房同步,早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。下面是基本的原理:
原理相对比较简单:
1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
2.mysql master收到dump请求,开始推送binary log给slave(也就是canal)
3.canal解析binary log对象(原始为byte流)
欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
\\n建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
\\n本站主要用到的插件有:
\\n1.wowslider
\\n2.畅言
\\n3.Editor.md
\\n4.highchart、echart\\n5.百度分享
\\n6.waterfall.js
\\n7.心知天气
\\n8.标签云
代码样式,这一点是不如WORDPRESS的插件了,不过已经可以了。
目前最常用的是highcharts跟echart,目前个人博客中的日志系统主要还是采用了highcharts,主要还是颜色什么的格调比较相符吧,其次是因为对echarts印象不太友好,比如下面做这张,打开网页后,缩小浏览器,百度的地域图却不能自适应,出现了越界,而highcharts的全部都能自适应调整。想起有次面试,我说我用的highcharts,面试官一脸嫌弃。。。(网上这么多人鄙视百度是假的?)
不过地图确确实实是echarts的优势,毕竟还是自家的东西了解自家,不过前段时间去看了看echarts的官网,已经不提供下载了。如果有需要,还是去csdn上搜索吧,或者替换为highmap。
#share a {
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ margin: 6px;
+ border-radius: 25px;
+ transition: all .4s;
+}
+/*主要的是替换掉backgound的背景图片*/
+#share a.bds_qzone {
+ background: url(http://image.wenzhihuai.com/t_QQZone.png) no-repeat;
+ background-size: 34px 34px;
+}
+
改完之后的效果。
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
\\njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k\\n
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
+
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
+
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
+
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
+
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
+
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
+
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。 典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
+
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
+
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些: -XX:+PrintGC 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用 输出形式:Application time: 0.5291524 seconds -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用 输出形式:Total time for which application threads were stopped: 0.0468229 seconds -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息 输出形式:
34.702: [GC {Heap before gc invocations=7:
+ def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
+from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)
+ to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
+ tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
+ compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+ the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+ ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+ rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
+ def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
+ from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
+ to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)
+ tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
+ compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+ the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+ ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+ rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+}
+, 0.0757599 secs]
+
-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。 常见配置汇总
-Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:设置年轻代大小 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -XX:MaxPermSize=n:设置持久代大小
-XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息 持久代并发收集次数 传统GC信息 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间,一般会提高应用的效率 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他 会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么 并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置: -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。 -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
`,49),l=[i];function d(p,t){return e(),s("div",null,l)}const m=a(r,[["render",d],["__file","JVM调优参数.html.vue"]]);export{m as default}; diff --git a/assets/SearchResult-Uuauwwzf.js b/assets/SearchResult-Uuauwwzf.js new file mode 100644 index 00000000..7dd7b8f4 --- /dev/null +++ b/assets/SearchResult-Uuauwwzf.js @@ -0,0 +1 @@ +import{u as U,f as ee,g as Y,h as j,Y as se,i as ae,t as le,j as te,k as D,l as A,m as re,n as B,p as a,q as _,s as F,v as I,x as ue,y as ie,z as ne,A as oe,R as ce,O as ve,B as pe,C as he,D as ye,E as de,F as me,G as fe,H as E,I as ge}from"./app-cS6i7hsH.js";const He="SEARCH_PRO_QUERY_HISTORY",y=U(He,[]),Re=()=>{const{queryHistoryCount:u}=E,o=u>0;return{enabled:o,queryHistory:y,addQueryHistory:t=>{o&&(y.value.length{y.value=[...y.value.slice(0,t),...y.value.slice(t+1)]}}},ke="SEARCH_PRO_RESULT_HISTORY",{resultHistoryCount:$}=E,d=U(ke,[]),Qe=()=>{const u=Y(),o=$>0,t=l=>u.resolve({name:l.key,..."anchor"in l?{hash:`#${l.anchor}`}:{}}).fullPath;return{enabled:o,resultHistory:d,addResultHistory:l=>{if(o){const i={link:t(l),display:l.display};"header"in l&&(i.header=l.header),d.value.length<$?d.value=[i,...d.value]:d.value=[i,...d.value.slice(0,$-1)]}},removeResultHistory:l=>{d.value=[...d.value.slice(0,l),...d.value.slice(l+1)]}}},xe=u=>{const o=ce(),t=j(),{search:l,terminate:i}=ve(),f=D(!1),g=pe([]);return he(()=>{const m=()=>{g.value=[],f.value=!1},x=ge(H=>{f.value=!0,H?l({type:"search",query:H,locale:t.value,options:o}).then(h=>{g.value=h,f.value=!1}).catch(h=>{console.error(h),m()}):m()},E.searchDelay);B([u,t],()=>x(u.value),{immediate:!0}),ye(()=>{i()})}),{searching:f,results:g}};var Se=ee({name:"SearchResult",props:{query:{type:String,required:!0},isFocusing:Boolean},emits:["close","updateQuery"],setup(u,{emit:o}){const t=Y(),l=j(),i=se(ae),{enabled:f,addQueryHistory:g,queryHistory:m,removeQueryHistory:x}=Re(),{enabled:H,resultHistory:h,addResultHistory:b,removeResultHistory:M}=Qe(),O=f||H,w=le(u,"query"),{results:R,searching:z}=xe(w),r=te({isQuery:!0,index:0}),p=D(0),c=D(0),P=A(()=>O&&(m.value.length>0||h.value.length>0)),S=A(()=>R.value.length>0),q=A(()=>R.value[p.value]||null),T=e=>t.resolve({name:e.key,..."anchor"in e?{hash:`#${e.anchor}`}:{}}).fullPath,G=()=>{const{isQuery:e,index:s}=r;s===0?(r.isQuery=!e,r.index=e?h.value.length-1:m.value.length-1):r.index=s-1},V=()=>{const{isQuery:e,index:s}=r;s===(e?m.value.length-1:h.value.length-1)?(r.isQuery=!e,r.index=0):r.index=s+1},J=()=>{p.value=p.value>0?p.value-1:R.value.length-1,c.value=q.value.contents.length-1},K=()=>{p.value=p.value留空
+`,r:{minutes:.02,words:5},y:"a",t:"关于我"},["/about-the-author/README.md"]],["v-cfe8b6c8","/cloudnative/",{d:1706066758e3,y:"a",t:""},["/cloudnative/README.md"]],["v-2e25198a","/database/",{d:1706182936e3,e:`DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。
+公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。
`,r:{minutes:11.93,words:3580},y:"a",t:"3.1 DevOps平台.md"},[":md"]],["v-06d5cd4c","/devops/gitlab-ci.html",{d:1706066758e3,e:`tt一面: +1.全程项目 +2.lc3,最长无重复子串,滑动窗口解决
+tt二面: +全程基础,一直追问 +1.java内存模型介绍一下 +2.volatile原理 +3.内存屏障,使用场景?(我提了在gc中有使用) +4.gc中具体是怎么使用内存屏障的,详细介绍 +5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 +6.线程内存分配方式,tlab相关 +7.happens-before介绍一下 +8.总线风暴是什么,怎么解决 +9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决 +10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222 +11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作
`,r:{minutes:1.15,words:344},y:"a",t:""},[":md"]],["v-0a27b598","/java/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html",{d:1706066758e3,e:`JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
+java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
+
服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
+
+stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG
前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 +该程序task主要分为三个模块: +console进行一些cron的配置(表达式、任务名称、任务组等); +schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发; +client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。 +整体架构跟github上开源的xxl-job类似,也可以参考一下。
`,r:{minutes:6.23,words:1870},y:"a",t:"一次jvm调优过程"},["/java/一次jvm调优过程.html","/java/一次jvm调优过程.md",":md"]],["v-660266c1","/java/%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C.html",{d:1706066758e3,e:`四个内存屏障指令:LoadLoad,
StoreStore,
LoadStore,
StoreLoad
在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值
+在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。
`,r:{minutes:.69,words:208},y:"a",t:"内存屏障"},["/java/内存屏障.html","/java/内存屏障.md",":md"]],["v-90f2343c","/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html",{d:1706186538e3,e:`在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。
+虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。
`,r:{minutes:4.83,words:1448},y:"a",t:"在 Spring 6 中使用虚拟线程"},["/java/在 Spring 6 中使用虚拟线程.html","/java/在 Spring 6 中使用虚拟线程.md",":md"]],["v-0b5d4df0","/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html",{d:1706066758e3,e:`做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。
+限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。
+限流可以应对:
+Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。
+使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。
+Spark Operator包括如下几个组件:
`,r:{minutes:3.5,words:1050},y:"a",t:"spark on k8s operator"},["/kubernetes/spark on k8s operator.html","/kubernetes/spark on k8s operator.md",":md"]],["v-22e4234a","/life/2017.html",{d:1706066758e3,e:`2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
+17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?
年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。
+想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。
+2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 +19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。
+从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。
`,r:{minutes:4.43,words:1329},y:"a",t:"2019"},[":md"]],["v-258f309e","/open-source-project/",{d:1706092222e3,e:`jfaowejfoewj
+`,r:{minutes:0,words:1},y:"a",t:""},["/open-source-project/README.md"]],["v-510415f9","/others/chatgpt.html",{d:1706186538e3,e:`想体验的可以去微信上搜索【旅行的树】公众号。
+openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。
+国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。
+国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。
+`,r:{minutes:9.96,words:2987},y:"a",t:"微信公众号-chatgpt智能客服搭建"},[":md"]],["v-3c114a75","/others/tesla.html",{d:1706186538e3,e:`如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。
+正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.
+ +`,r:{minutes:4.5,words:1351},y:"a",t:"Tesla api"},[":md"]],["v-37ac02fe","/personalWebsite/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html",{d:1706182936e3,e:`各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。 +大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。
`,r:{minutes:6.79,words:2038},y:"a",t:"1.历史与架构"},["/personalWebsite/1.历史与架构.html","/personalWebsite/1.历史与架构.md",":md"]],["v-523e9724","/personalWebsite/10.%E7%BD%91%E7%AB%99%E9%87%8D%E6%9E%84.html",{d:1706182936e3,e:`因原本的
+`,r:{minutes:.03,words:9},y:"a",t:"10.网站重构"},["/personalWebsite/10.网站重构.html","/personalWebsite/10.网站重构.md",":md"]],["v-65b5c81c","/personalWebsite/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html",{d:1706182936e3,e:`首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。
`,r:{minutes:6.82,words:2046},y:"a",t:"2.Lucene的使用"},["/personalWebsite/2.Lucene的使用.html","/personalWebsite/2.Lucene的使用.md",":md"]],["v-731a76b6","/personalWebsite/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html",{d:1706182936e3,e:`先看一下Quartz的架构图:
+ +欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
`,r:{minutes:9.64,words:2891},y:"a",t:"4.日志系统.md"},["/personalWebsite/4.日志系统.html","/personalWebsite/4.日志系统.md",":md"]],["v-3efc517e","/personalWebsite/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html",{d:1706182936e3,e:`欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。
+洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。
先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:
+ +由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:
`,r:{minutes:5.77,words:1732},y:"a",t:"6.数据库备份"},["/personalWebsite/6.数据库备份.html","/personalWebsite/6.数据库备份.md",":md"]],["v-8658e60a","/personalWebsite/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html",{d:1706182936e3,e:`欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
+建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
+本站主要用到的插件有:
+1.wowslider
+2.畅言
+3.Editor.md
+4.highchart、echart
+5.百度分享
+6.waterfall.js
+7.心知天气
+8.标签云
系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 +缓存常用语: +数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透 +可查看Redis实战(一) 使用缓存合理性
`,r:{minutes:11.96,words:3588},y:"a",t:"一、概述"},["/redis/redis缓存.html","/redis/redis缓存.md",":md"]],["v-43e9e6b0","/about-the-author/personal-life/wewe.html",{d:1706244827e3,e:`oifjwoiej
+`,r:{minutes:.02,words:5},y:"a",t:"自我介绍"},[":md"]],["v-04e32b44","/bigdata/spark/elastic-spark.html",{d:1706182936e3,e:`Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
+(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
+(2)数据RDBMS:mysql、oracle、mssql
+(3)NOSQL数据库:HBase、ES、Redis
+(4)消息对象:Redis
直接用idea下载代码https://github.com/elastic/elasticsearch.git +
+切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好 +
`,r:{minutes:.52,words:157},y:"a",t:"【elasticsearch】源码debug"},["/database/elasticsearch/elasticsearch源码debug.html","/database/elasticsearch/elasticsearch源码debug.md",":md"]],["v-3f2a15ee","/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html",{d:1706183164e3,e:`本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。
+QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。 +DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。
`,r:{minutes:8.92,words:2676},y:"a",t:"【elasticsearch】搜索过程详解"},["/database/elasticsearch/【elasticsearch】搜索过程详解.html","/database/elasticsearch/【elasticsearch】搜索过程详解.md",":md"]],["v-aa93dec0","/database/mysql/1mysql.html",{d:1706092222e3,e:`第一次听到执行计划还是挺懵的,感觉像是存储函数什么样的东西,后来发现,居然是explain
+`,r:{minutes:.14,words:41},y:"a",t:"执行计划explain"},["/database/mysql/执行计划explain.html","/database/mysql/执行计划explain.md",":md"]],["v-0a0b7a54","/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html",{d:1706092222e3,e:`在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。
+将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。
+禁用原因:
+1.命中率低
+2.写时所有都失效
+禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。
`,r:{minutes:.34,words:101},y:"a",t:"数据库缓存"},["/database/mysql/数据库缓存.html","/database/mysql/数据库缓存.md",":md"]],["v-d7d7df80","/middleware/kafka/kafka.html",{d:1706204085e3,e:`1、请说明什么是Apache Kafka? +Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。
+2、说说Kafka的使用场景? +①异步处理 +②应用解耦 +③流量削峰 +④日志处理 +⑤消息通讯等。
+3、使用Kafka有什么优点和缺点? +优点: +①支持跨数据中心的消息复制; +②单机吞吐量:十万级,最大的优点,就是吞吐量高; +③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源; +④时效性:ms级; +⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用; +⑥消息可靠性:经过参数优化配置,消息可以做到0丢失; +⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。
`,r:{minutes:17.01,words:5103},y:"a",t:"kafka面试题"},[":md"]],["v-7d9b9318","/middleware/zookeeper/zookeeper.html",{d:1706204085e3,e:`留空
+`,r:{minutes:.01,words:3},y:"a",t:"Zookeeper"},[":md"]],["v-3706649a","/404.html",{y:"p",t:""},[]],["v-71b3ae87","/interview/",{y:"p",t:"Interview"},[]],["v-14c69af4","/java/",{y:"p",t:"Java"},[]],["v-14e6315a","/life/",{y:"p",t:"Life"},[]],["v-25b47c13","/others/",{y:"p",t:"Others"},[]],["v-525c5e4d","/personalWebsite/",{y:"p",t:"Personal Website"},[]],["v-5d3e6196","/about-the-author/personal-life/",{y:"p",t:"Personal Life"},[]],["v-60fc7530","/bigdata/spark/",{y:"p",t:"Spark"},[]],["v-02bc92be","/bigdata/",{y:"p",t:"Bigdata"},[]],["v-380c630d","/database/elasticsearch/",{y:"p",t:"Elasticsearch"},[]],["v-21ba2ec8","/database/mysql/",{y:"p",t:"Mysql"},[]],["v-a32329e6","/middleware/kafka/",{y:"p",t:"Kafka"},[]],["v-4d194044","/middleware/",{y:"p",t:"Middleware"},[]],["v-6e21a4b2","/middleware/zookeeper/",{y:"p",t:"Zookeeper"},[]],["v-5bc93818","/category/",{y:"p",t:"分类",I:!1},[]],["v-744d024e","/tag/",{y:"p",t:"标签",I:!1},[]],["v-e52c881c","/article/",{y:"p",t:"文章",I:!1},[]],["v-154dc4c4","/star/",{y:"p",t:"星标",I:!1},[]],["v-01560935","/timeline/",{y:"p",t:"时间轴",I:!1},[]]];var Rs=D({name:"Vuepress",setup(){const e=Wf();return()=>s(e.value)}}),k3=()=>E3.reduce((e,[t,n,r,a])=>(e.push({name:t,path:n,component:Rs,meta:r},{path:n.endsWith("/")?n+"index.html":n.substring(0,n.length-5),redirect:n},...a.map(l=>({path:l===":md"?n.substring(0,n.length-5)+".md":l,redirect:n}))),e),[{name:"404",path:"/:catchAll(.*)",component:Rs}]),x3=pp,T3=()=>{const e=Yp({history:x3(Cl("/")),routes:k3(),scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{var r;(t.path!==n.path||n===xt)&&([t.meta._data]=await Promise.all([kt.resolvePageData(t.name),(r=Hi[t.name])==null?void 0:r.__asyncLoader()]))}),e},L3=e=>{e.component("ClientOnly",aa),e.component("Content",Zi)},A3=(e,t,n)=>{const r=ps(()=>t.currentRoute.value.path),a=ps(()=>kt.resolveRouteLocale(hn.value.locales,r.value)),l=Dl(r,()=>t.currentRoute.value.meta._data),o=y(()=>kt.resolveLayouts(n)),i=y(()=>kt.resolveSiteLocaleData(hn.value,a.value)),c=y(()=>kt.resolvePageFrontmatter(l.value)),u=y(()=>kt.resolvePageHeadTitle(l.value,i.value)),d=y(()=>kt.resolvePageHead(u.value,c.value,i.value)),f=y(()=>kt.resolvePageLang(l.value,i.value)),p=y(()=>kt.resolvePageLayout(l.value,o.value));return e.provide(zf,o),e.provide(Fi,l),e.provide(ji,c),e.provide(jf,u),e.provide(Wi,d),e.provide(Gi,f),e.provide(Ki,p),e.provide(Il,a),e.provide(Ui,i),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>c.value},$head:{get:()=>d.value},$headTitle:{get:()=>u.value},$lang:{get:()=>f.value},$page:{get:()=>l.value},$routeLocale:{get:()=>a.value},$site:{get:()=>hn.value},$siteLocale:{get:()=>i.value},$withBase:{get:()=>Ee}}),{layouts:o,pageData:l,pageFrontmatter:c,pageHead:d,pageHeadTitle:u,pageLang:f,pageLayout:p,routeLocale:a,siteData:hn,siteLocaleData:i}},S3=()=>{const e=Ff(),t=Pl(),n=Z([]),r=()=>{e.value.forEach(l=>{const o=C3(l);o&&n.value.push(o)})},a=()=>{document.documentElement.lang=t.value,n.value.forEach(l=>{l.parentNode===document.head&&document.head.removeChild(l)}),n.value.splice(0,n.value.length),e.value.forEach(l=>{const o=P3(l);o!==null&&(document.head.appendChild(o),n.value.push(o))})};rt(Kf,a),ge(()=>{r(),a(),ve(()=>e.value,a)})},C3=([e,t,n=""])=>{const r=Object.entries(t).map(([i,c])=>oe(c)?`[${i}=${JSON.stringify(c)}]`:c===!0?`[${i}]`:"").join(""),a=`head > ${e}${r}`;return Array.from(document.querySelectorAll(a)).find(i=>i.innerText===n)||null},P3=([e,t,n])=>{if(!oe(e))return null;const r=document.createElement(e);return Sl(t)&&Object.entries(t).forEach(([a,l])=>{oe(l)?r.setAttribute(a,l):l===!0&&r.setAttribute(a,"")}),oe(n)&&r.appendChild(document.createTextNode(n)),r},I3=Sf,R3=async()=>{var n;const e=I3({name:"VuepressApp",setup(){var r;S3();for(const a of Dr)(r=a.setup)==null||r.call(a);return()=>[s(oc),...Dr.flatMap(({rootComponents:a=[]})=>a.map(l=>s(l)))]}}),t=T3();L3(e),A3(e,t,Dr);for(const r of Dr)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:hn}));return e.use(t),{app:e,router:t}};R3().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{W4 as A,lt as B,ge as C,Cn as D,oe as E,B3 as F,Sl as G,ao as H,k0 as I,e3 as O,J4 as R,no as Y,Li as a,Re as b,M3 as c,R3 as createVueApp,Ai as d,$3 as e,D as f,De as g,ft as h,lo as i,or as j,Z as k,y as l,Ce as m,ve as n,O3 as o,s as p,G4 as q,Ye as r,q4 as s,Sn as t,V3 as u,z4 as v,U1 as w,Gu as x,K4 as y,j4 as z}; +function __vite__mapDeps(indexes) { + if (!__vite__mapDeps.viteFileDeps) { + __vite__mapDeps.viteFileDeps = ["assets/index.html-GO9KhDWT.js","assets/plugin-vue_export-helper-x3n3nnut.js","assets/index.html--YxqP697.js","assets/index.html-CmwrSuW4.js","assets/index.html-OXmsW7Ba.js","assets/index.html-7Jwn4Cbi.js","assets/devops-ping-tai.html-Pr-n_ibi.js","assets/gitlab-ci.html-9dvcTJNs.js","assets/jenkins-x.html--QxcKVNb.js","assets/tekton.html-5SA8R3wN.js","assets/index.html-2mdvLUFa.js","assets/tiktok2023.html-Ih4GrSaS.js","assets/JVM调优参数.html-adpG87p4.js","assets/lucene搜索原理.html-xLQYDnip.js","assets/serverlog.html-f8i7CjpW.js","assets/一次jvm调优过程.html-ObKNMeJg.js","assets/内存屏障.html-P7fE8DoQ.js","assets/在 Spring 6 中使用虚拟线程.html-lrPVTD8u.js","assets/基于kubernetes的分布式限流.html-FM9j_CKw.js","assets/index.html-1G2UU7KQ.js","assets/spark on k8s operator.html-oBat6Q5m.js","assets/2017.html-BbxR5q6z.js","assets/2018.html-iLHJNmQ1.js","assets/2019.html-ogBQCkli.js","assets/index.html-YcQRCdzL.js","assets/chatgpt.html-lQQa_oJF.js","assets/tesla.html-2Y58-poF.js","assets/1.历史与架构.html-X_ZVzsKc.js","assets/10.网站重构.html-vlubYdUg.js","assets/2.Lucene的使用.html-5sj9Z-FP.js","assets/3.定时任务.html-CtCjmCiV.js","assets/4.日志系统.html-Gyuvpz3_.js","assets/5.小集群部署.html-z5KJNYuo.js","assets/6.数据库备份.html-GrbFXSWT.js","assets/7.那些牛逼的插件.html-xUNU4jTw.js","assets/8.基于贝叶斯的情感分析.html-DmNVFgOS.js","assets/9.网站性能优化.html-MC-9j6vs.js","assets/index.html-9KTewoYW.js","assets/redis缓存.html-2QC7Jkrn.js","assets/wewe.html-MSrbCURC.js","assets/elastic-spark.html-NDvsW2Dx.js","assets/elasticsearch源码debug.html-riyCnJZU.js","assets/【elasticsearch】搜索过程详解.html-ga4ihhfd.js","assets/1mysql.html-OHVRcqVz.js","assets/gap锁.html-0qMpGSdo.js","assets/执行计划explain.html-y5jML1O_.js","assets/数据库缓存.html-fj1Jh_qh.js","assets/kafka.html-FefoBwLN.js","assets/zookeeper.html-bxhlqtiT.js","assets/404.html-tbxpD8LH.js","assets/index.html-JRhiMr7F.js","assets/index.html-UdEIAh49.js","assets/index.html-GqCa2uIM.js","assets/index.html-Pmc9G9cb.js","assets/index.html-OwsuoqvI.js","assets/index.html-dfS5oaDz.js","assets/index.html-AOPdgBmB.js","assets/index.html-Pv8xpMqr.js","assets/index.html-IuOpNwku.js","assets/index.html-PzLMN4bj.js","assets/index.html-6QAvnWzZ.js","assets/index.html-jG6_D9tR.js","assets/index.html-L2EY0waG.js","assets/index.html-_KtgcszN.js","assets/index.html-IefXkqnJ.js","assets/index.html-GqC77IMP.js","assets/index.html-QFJKSvrT.js","assets/index.html--QH2zN9-.js"] + } + return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) +} diff --git a/assets/chatgpt.html-lQQa_oJF.js b/assets/chatgpt.html-lQQa_oJF.js new file mode 100644 index 00000000..05f55471 --- /dev/null +++ b/assets/chatgpt.html-lQQa_oJF.js @@ -0,0 +1,110 @@ +import{_ as e}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as p,o,c as i,a as s,d as n,b as c,e as a}from"./app-cS6i7hsH.js";const l={},r=a(`想体验的可以去微信上搜索【旅行的树】公众号。
openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。
国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。
国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。
之后再搜索框填openai进行下单购买即可。
openai在国内不提供服务的,而且也通过ip识别是不是在国内,解决办法用vpn也行,或者,自己去买一台国外的服务器也行。我这里使用的是腾讯云轻量服务器,最低配置54元/月,选择windows的主要原因毕竟需要注册openai,需要看页面,同时也可以搭建nginx,当然,用ubuntu如果能自己搞界面也行。
购买完之后,就可以直接打开openai的官网了,然后去https://platform.openai.com/signup官网里注册,注册过程具体就不讲了,讲下核心问题——短信验证码
然后回sms查看验证码。
注册成功之后就可以在chatgpt里聊天啦,能够识别各种语言,发起多轮会话的时候,可能回出现访问超过限制什么的。
通过chatgpt聊天不是我们最终想要的,我们需要的是在微信公众号也提供智能客服的聊天回复,所以我们需要在通过openai的api来进行调用。
跟页面一样,OpenAI的调用也是不能再国内访问的,这里,我们使用同一台服务器来搭建nginx,还是保留使用windows吧,主要还是得注意下面这段话,如果API key被泄露了,OpenAI可能会自动重新更新你的API key,这个规则似乎是API key如果被多个ip使用,就会触发这个规则,调试阶段还是尽量使用windows的服务器吧,万一被更新了,还能去页面上重新找到。
Do not share your API key with others, or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may also automatically rotate any API key that we've found has leaked publicly.
+
windows的安装过程参考网上的来,我们只需要添加下面这个配置即可,原理主要是将调用OpenAI的接口全部往官网转发。
location /v1/completions {
+ proxy_pass https://api.openai.com/v1/completions;
+ }
+
然后使用下面的方法进行调试即可:
POST http://YOUR IP/v1/completions
+Authorization: Bearer YOUR_API_KEY
+Content-Type: application/json
+
+{
+ "model": "text-davinci-003",
+ "prompt": "Say this is a test",
+ "max_tokens": 7,
+ "temperature": 0
+}
+
网上有很多关于微信通过chatgpt回复的文章,有些使用自己微信号直接做为载体,因为要扫码网页登陆,而且是网页端在国外,很容易被封;有些是使用公众号,相对来说,公众号被封也不至于导致个人微信号出问题。
微信公众平台提供了微信云托管,无需鉴权,比其他方式都方便不少,可以免费试用3个月,继续薅羊毛,当然,如果自己开发能力足够,也可以自己从0开始开发。
提供了各种语言的模版,方便快速开发,OpenAI官方提供的sdk是node和python,这里我们选择express(node)。
微信官方的源码在这,https://github.com/WeixinCloud/wxcloudrun-express,我们直接fork一份自己来开发。
一个简单的消息回复功能(无db),直接在我们的index.js里添加如下代码。
const configuration = new Configuration({
+ apiKey: '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',
+ basePath: 'http://43.153.15.174/v1'
+});
+const openai = new OpenAIApi(configuration);
+
+async function simpleResponse(prompt) {
+ const completion = await openai.createCompletion({
+ model: 'text-davinci-003',
+ prompt,
+ max_tokens: 1024,
+ temperature: 0.1,
+ });
+ const response = (completion?.data?.choices?.[0].text || 'AI 挂了').trim();
+ return strip(response, ['\\n', 'A: ']);
+}
+
+app.post("/message/simple", async (req, res) => {
+ console.log('消息推送', req.body)
+ // 从 header 中取appid,如果 from-appid 不存在,则不是资源复用场景,可以直接传空字符串,使用环境所属账号发起云调用
+ const appid = req.headers['x-wx-from-appid'] || ''
+ const {ToUserName, FromUserName, MsgType, Content, CreateTime} = req.body
+ console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)
+ if (MsgType === 'text') {
+ message = await simpleResponse(Content)
+ res.send({
+ ToUserName: FromUserName,
+ FromUserName: ToUserName,
+ CreateTime: CreateTime,
+ MsgType: 'text',
+ Content: message,
+ })
+ } else {
+ res.send('success')
+ }
+})
+
本地可以直接使用http请求测试,成功调用之后直接提交发布即可,在微信云托管上需要先授权代码库,即可使用云托管的流水线,一键发布。注意,api_base和api_key可以填写在服务设置-基础信息那里,一个是OPENAI_API_BASE,一个是OPENAI_API_KEY,服务会自动从环境变量里取。
提交代码只github或者gitee都可以,值得注意的是,OpenAI判断key泄露的规则,不知道是不是判断调用的ip地址不一样,还是github的提交记录里含有这块,有点玄学,同样的key本地调用一次,然后在云托管也调用的话,OpenAI就很容易把key给重新更新。
部署完之后,云托管也提供了云端调试功能,相当于在服务里发送了http请求。这一步很重要,如果没有调用成功,则无法进行云托管消息推送。
这里填上你自己的url,我们这里配置的是/meesage/simple,如果没有成功,需要进行下面步骤进行排查:
(1)服务有没有正常启动,看日志
(2)端口有没有设置错误,这个很多次没有注意到
保存成功之后,就可以在微信公众号里测试了。
体验还可以
很多OpenAI的回答都要几十秒,有的甚至更久,比如对chatgpt问“写一篇1000字关于深圳的文章”,就需要几十秒,而微信的主动回复接口,是需要我们3s内返回给用户。
订阅号的消息推送分几种:
根据微信官方文档,没有认证的公众号是没有调用主动回复接口权限的,https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html
对于有微信认证的订阅号或者服务号,可以调用微信官方的/cgi-bin/message/custom/send接口来实现主动回复,但是对于个人的公众号,没有权限调用,只能尝试别的办法。想来想去,只能在3s内返回让用户重新复制发送的信息,同时后台里保存记录异步调用,用户重新发送的时候再从数据库里提取回复。
1.先往数据库存一条 回复记录,把用户的提问存下来,以便后续查询。设置回复的内容为空,设置状态为 回复中(thinking)。
// 因为AI响应比较慢,容易超时,先插入一条记录,维持状态,待后续更新记录。
+ await Message.create({
+ fromUser: FromUserName,
+ response: '',
+ request: Content,
+ aiType: AI_TYPE_TEXT, // 为其他AI回复拓展,比如AI作画
+ });
+
2.抽象一个 chatGPT 请求方法 getAIMessage
,函数内部得到 GPT 响应后,会更新之前那条记录(通过用户id & 用户提问 查询),把状态更新为 已回答(answered),并把回复内容更新上。
// 成功后,更新记录
+ await Message.update(
+ {
+ response: response,
+ status: MESSAGE_STATUS_ANSWERED,
+ },
+ {
+ where: {
+ fromUser: FromUserName,
+ request: Content,
+ },
+ },
+ );
+
3.前置增加一些判断,当用户在请求时,如果 AI 还没完成响应,直接回复用户 AI 还在响应,让用户过一会儿再重试。如果 AI 此时已响应完成,则直接把 内容返回给用户。
// 找一下,是否已有记录
+ const message = await Message.findOne({
+ where: {
+ fromUser: FromUserName,
+ request: Content,
+ },
+ });
+
+ // 已回答,直接返回消息
+ if (message?.status === MESSAGE_STATUS_ANSWERED) {
+ return \`[GPT]: \${message?.response}\`;
+ }
+
+ // 在回答中
+ if (message?.status === MESSAGE_STATUS_THINKING) {
+ return AI_THINKING_MESSAGE;
+ }
+
4.最后就是一个 Promise.race
const message = await Promise.race([
+ // 3秒微信服务器就会超时,超过2.9秒要提示用户重试
+ sleep(2900).then(() => AI_THINKING_MESSAGE),
+ getAIMessage({ Content, FromUserName }),
+ ]);
+
这样子大概就能实现超时之前返回了。
掉接口是一次性的,一次接口调用完之后怎么做到下一次通话的时候,还能继续保持会话,是不是应该类似客户端与服务端那种有个session这种,但是实际上在openai里是没有session这种东西的,令人震惊的是,chatgpt里是通过前几次会话拼接起来一起发送给chatgpt里的,需要通过回车符来拼接。
async function buildCtxPrompt({ FromUserName }) {
+ // 获取最近10条对话
+ const messages = await Message.findAll({
+ where: {
+ fromUser: FromUserName,
+ aiType: AI_TYPE_TEXT,
+ },
+ limit: 10,
+ order: [['updatedAt', 'ASC']],
+ });
+ // 只有一条的时候,就不用封装上下文了
+ return messages.length === 1
+ ? messages[0].request
+ : messages
+ .map(({ response, request }) => \`Q: \${request}\\n A: \${response}\`)
+ .join('\\n');
+}
+
之后就可以实现会话之间的保存通信了。
chatgpt毕竟也是新上线的,火热是肯定的,聊天窗口只能开几个,api调用的话,也是有限频的,但是规则具体没有找到,只是在调用次数过多的时候会报429的错误,出现之后就需要等待一个小时左右。
对于这个的解决办法只能是多开几个账号,一旦429就只能换个账号重试了。
没有找到详细的规则,凭个人经验的话,可能github提交的代码会被扫描,可能ip调用的来源不一样,最好还是开发一个秘钥,生产一个秘钥吧。
我们这里用的模型算法是text-davinci-003,具体可以参考:https://platform.openai.com/docs/models/overview,也算是一个比较老的样本了吧
`,83),u=s("code",null,"text-davinci-003",-1),d={href:"https://link.juejin.cn/?target=https%3A%2F%2Fplatform.openai.com%2Fdocs%2Fguides%2Ffine-tuning",target:"_blank",rel:"noopener noreferrer"},k=a('有时候消息没有回复,真的不是我们的问题,chatgpt毕竟太火了,官网的这个能力都经常挂掉,也可以订阅官网修复的通知,一旦修复则会发邮件告知你。
参考:https://juejin.cn/post/7200769439335546935
记得去微信关注【旅行的树】公众号体验
代码地址:https://github.com/Zephery/wechat-gpt
想体验的可以去微信上搜索【旅行的树】公众号。
\\nopenai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。
\\n国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。
\\n国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。
\\n","autoDesc":true}');export{t as data}; diff --git a/assets/devops-ping-tai.html-Pr-n_ibi.js b/assets/devops-ping-tai.html-Pr-n_ibi.js new file mode 100644 index 00000000..26718294 --- /dev/null +++ b/assets/devops-ping-tai.html-Pr-n_ibi.js @@ -0,0 +1,246 @@ +import{_ as i}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as l,o as p,c,a as n,d as s,b as e,e as t}from"./app-cS6i7hsH.js";const o={},u=n("h1",{id:"_3-1-devops平台-md",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#_3-1-devops平台-md","aria-hidden":"true"},"#"),s(" 3.1 DevOps平台.md")],-1),d=n("p",null,"DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。",-1),r=n("p",null,"公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。",-1),v=n("h2",{id:"一、自由风格的软件项目",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#一、自由风格的软件项目","aria-hidden":"true"},"#"),s(" 一、自由风格的软件项目")],-1),m={href:"https://www.cnblogs.com/w1570631036/p/9861473.html",target:"_blank",rel:"noopener noreferrer"},g=t(`完成了以上的东西,不过由于太过于简单,导致只能进行单条线的CICD,而且CI仅仅实现了打包,没有将CD的过程一同串行起来。简单来说就是,用户点击了构建只是能够打出一个镜像,但是如果要部署到kubernetes,还是需要在应用里手动更换一下镜像版本。总体而言,这个版本的jenkins我们使用的还是单点的,不足以支撑构建量比较大的情况,甚至如果当前服务挂了,断网了,整一块的构建功能都不能用。
<project>
+ <actions/>
+ <description>xxx</description>
+ <properties>
+ <hudson.model.ParametersDefinitionProperty>
+ <parameterDefinitions>
+ <hudson.model.TextParameterDefinition>
+ <name>buildParam</name>
+ <defaultValue>v1</defaultValue>
+ </hudson.model.TextParameterDefinition>
+ <hudson.model.TextParameterDefinition>
+ <name>codeBranch</name>
+ <defaultValue>master</defaultValue>
+ </hudson.model.TextParameterDefinition>
+ </parameterDefinitions>
+ </hudson.model.ParametersDefinitionProperty>
+ </properties>
+ <scm class="hudson.plugins.git.GitSCM">
+ <configVersion>2</configVersion>
+ <userRemoteConfigs>
+ <hudson.plugins.git.UserRemoteConfig>
+ <url>http://xxxxx.git</url>
+ <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>
+ </hudson.plugins.git.UserRemoteConfig>
+ </userRemoteConfigs>
+ <branches>
+ <hudson.plugins.git.BranchSpec>
+ <name>\${codeBranch}</name>
+ </hudson.plugins.git.BranchSpec>
+ </branches>
+ <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+ <extensions/>
+ </scm>
+ <builders>
+ <hudson.tasks.Shell>
+ <command>ls</command>
+ </hudson.tasks.Shell>
+ <hudson.tasks.Maven>
+ <targets>clean package install -Dmaven.test.skip=true</targets>
+ <mavenName>mvn3.5.4</mavenName>
+ </hudson.tasks.Maven>
+ <com.cloudbees.dockerpublish.DockerBuilder>
+ <server>
+ <uri>unix:///var/run/docker.sock</uri>
+ </server>
+ <registry>
+ <url>http://xxxx</url>
+ </registry>
+ <repoName>xxx/xx</repoName>
+ <forcePull>true</forcePull>
+ <dockerfilePath>Dockerfile</dockerfilePath>
+ <repoTag>\${buildParam}</repoTag>
+ <skipTagLatest>true</skipTagLatest>
+ </com.cloudbees.dockerpublish.DockerBuilder>
+ </builders>
+ <publishers>
+ <com.xxxx.notifications.Notifier/>
+ </publishers>
+</project>
+
上面的过程也仍然没有没住DevOps的流程,人工干预的东西依旧很多,由于上级急需产出,所以只能将就着继续下去。我们将构建、部署每个当做一个小块,一个CICD的过程可以选择构建、部署,花了很大的精力,完成了串行化的别样的CICD。 以下图为例,整个流程的底层为:paas平台-jenkins-kakfa-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发构建-jenkins-kafka-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发部署。
目前实现了串行化的CICD构建部署,之后考虑实现多个CICD并行,并且一个CICD能够调用另一个CICD,实际运行中,出现了一大堆问题。由于经过的组件太多,一次cicd的运行报错,却很难排查到问题出现的原因,业务方的投诉也开始慢慢多了起来,只能说劝导他们不要用这个功能。
没有CICD,就无法帮助公司上容器云,无法合理的利用容器云的特性,更无法走上云原生的道路。于是,我们决定另谋出路。
由于之前的CICD问题太多,特别是经过的组件太多了,导致出现问题的时候无法正常排查,为了能够更加稳定可靠,还是决定了要更换一下底层。 我们重新审视了下pipeline,觉得这才是正确的做法,可惜不知道如果做成一个产品样子的东西,用户方Dockerfile都不怎么会写,你让他写一个Jenkinsfile?不合理!在此之外,我们看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自带了cicd的工具,需要配置一下runner,然后配置一下.gitlab-ci.yml写一下程序的cicd过程即可,构建镜像的时候我们使用的是kaniko,整个gitlab的cicd在我们公司小项目中大范围使用,但是学习成本过高,尤其是引入了kaniko之后,还是寻找一个产品化的CICD方案。
`,9),k=n("strong",null,"分布式构建jenkins x",-1),b={href:"https://github.com/jenkins-x/jenkins-x-image",target:"_blank",rel:"noopener noreferrer"},h=n("figure",null,[n("img",{src:"http://image.wenzhihuai.com/images/201908170612301464716259.png",alt:"",tabindex:"0",loading:"lazy"}),n("figcaption")],-1),x=n("p",null,[n("strong",null,"serverless jenkins"),s(" 好像跟谷歌的tekton相关,用了下,没调通,只能用于GitHub。感觉还不如直接使用tekton。")],-1),q=n("p",null,[n("strong",null,"阿里云云效"),s(" 提供了图形化配置DevOps流程,支持定时触发,可惜没有跟gitlab触发结合,如果需要个公司级的DevOps,需要将公司的jira、gitlab、jenkins等集合起来,但是图形化jenkins pipeline是个特别好的参考方向,可以结合阿里云云效来做一个自己的DevOps产品。")],-1),f=n("p",null,[n("strong",null,"微软Pipeline"),s(" 微软也是提供了DevOps解决方案的,也是提供了yaml格式的写法,即:在右边填写完之后会转化成yaml。如果想把DevOps打造成一款产品,这样的设计显然不是最好的。")],-1),_=n("figure",null,[n("img",{src:"http://image.wenzhihuai.com/images/201908100346011648784131.png",alt:"",tabindex:"0",loading:"lazy"}),n("figcaption")],-1),D=n("strong",null,"谷歌tekton",-1),y={href:"https://www.jianshu.com/p/8871b7ea7d6e",target:"_blank",rel:"noopener noreferrer"},j=t(`在调研DockOne以及各个产商的DevOps产品时,发现,真的只有阿里云的云效才是真正比较完美的DevOps产品,用户不需要知道pipeline的语法,也不需要掌握kubernetes的相关知识,甚至不用写yaml文件,对于开发、测试来说简直就是神一样的存在了。云效对小公司(创业公司)免费,但是有一定的量之后,就要开始收费了。在调研了一番云效的东西之后,发现云效也是基于jenkins x改造的,不过阿里毕竟人多,虽然能约莫看出是pipeline的语法,但是阿里彻底改造成了能够使用yaml来与后台交互。 下面是以阿里云的云效界面以及配合jenkins的pipeline语法来讲解:
PMD是一款可拓展的静态代码分析器它不仅可以对代码分析器,它不仅可以对代码风格进行检查,还可以检查设计、多线程、性能等方面的问题。阿里云的是简单的集成了一下而已,对于我们来说,底层使用了sonar来接入,所有的代码扫描结果都接入了sonar。
stage('Clone') {
+ steps{
+ git branch: 'master', credentialsId: 'xxxx', url: "xxx"
+ }
+}
+stage('check') {
+ steps{
+ container('maven') {
+ echo "mvn pmd:pmd"
+ }
+ }
+}
+
Java的单元测试一般用的是Junit,在阿里云中,使用了surefire插件,用来在maven构建生命周期的test phase执行一个应用的单元测试。它会产生两种不同形式的测试结果报告。我这里就简单的过一下,使用"mvn test"命令来代替。
+stage('Clone') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+ }
+}
+stage('test') {
+ steps{
+ container('maven') {
+ sh "mvn test"
+ }
+ }
+}
+
镜像的构建比较想使用kaniko,尝试找了不少方法,到最后还是只能使用dind(docker in docker),挂载宿主机的docker来进行构建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,挂载的时候需要配置一下config.json,然后挂载到容器的/root/.docker目录,才能在容器中使用docker。
为什么不推荐dind:挂载了宿主机的docker,就可以使用docker ps查看正在运行的容器,也就意味着可以使用docker stop、docker rm来控制宿主机的容器,虽然kubernetes会重新调度起来,但是这一段的重启时间极大的影响业务。
+stage('下载代码') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+ script {
+ build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+ }
+ }
+}
+stage('打包并构建镜像') {
+ steps{
+ container('maven') {
+ echo "3.Build Docker Image Stage"
+ sh "mvn clean install -Dmaven.test.skip=true"
+ sh "docker build -f xxx/Dockerfile -t xxxxxx:\${build_tag} ."
+ sh "docker push xxxxxx:\${build_tag}"
+ }
+ }
+}
+
+
CD过程有点困难,由于我们的kubernetes平台是图形化的,类似于阿里云,用户根本不需要自己写deployment,只需要在图形化界面做一下操作即可部署。对于CD过程来说,如果应用存在的话,就可以直接替换掉镜像版本即可,如果没有应用,就提供个简单的界面让用户新建应用。当然,在容器最初推行的时候,对于用户来说,一下子需要接受docker、kubernetes、helm等概念是十分困难的,不能一个一个帮他们写deployment这些yaml文件,只能用helm创建一个通用的spring boot或者其他的模板,然后让业务方修改自己的配置,每次构建的时候只需要替换镜像即可。
def tmp = sh (
+ returnStdout: true,
+ script: "kubectl get deployment -n \${namespace} | grep \${JOB_NAME} | awk '{print \\$1}'"
+)
+//如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+if(tmp.equals('')){
+ sh "helm init --client-only"
+ sh """helm repo add mychartmuseum http://xxxxxx \\
+ --username myuser \\
+ --password=mypass"""
+ sh """helm install --set name=\${JOB_NAME} \\
+ --set namespace=\${namespace} \\
+ --set deployment.image=\${image} \\
+ --set deployment.imagePullSecrets=\${harborProject} \\
+ --name \${namespace}-\${JOB_NAME} \\
+ mychartmuseum/soa-template"""
+}else{
+ println "已经存在,替换镜像"
+ //epaas中一个pod的容器名称需要带上"-0"来区分
+ sh "kubectl set image deployment/\${JOB_NAME} \${JOB_NAME}-0=\${image} -n \${namespace}"
+}
+
+
代码扫描,单元测试,构建镜像三个并行运行,等三个完成之后,在进行部署
pipeline:
pipeline {
+ agent {
+ label "jenkins-maven"
+ }
+ stages{
+ stage('代码扫描,单元测试,镜像构建'){
+ parallel {
+ stage('并行任务一') {
+ agent {
+ label "jenkins-maven"
+ }
+ stages('Java代码扫描') {
+ stage('Clone') {
+ steps{
+ git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+ }
+ }
+ stage('check') {
+ steps{
+ container('maven') {
+ echo "$BUILD_NUMBER"
+ }
+ }
+ }
+ }
+ }
+ stage('并行任务二') {
+ agent {
+ label "jenkins-maven"
+ }
+ stages('Java单元测试') {
+ stage('Clone') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+ }
+ }
+ stage('test') {
+ steps{
+ container('maven') {
+ echo "3.Build Docker Image Stage"
+ sh "mvn -v"
+ }
+ }
+ }
+ }
+ }
+ stage('并行任务三') {
+ agent {
+ label "jenkins-maven"
+ }
+ stages('java构建镜像') {
+ stage('Clone') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+ script {
+ build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+ }
+ }
+ }
+ stage('Build') {
+ steps{
+ container('maven') {
+ echo "3.Build Docker Image Stage"
+ sh "mvn clean install -Dmaven.test.skip=true"
+ sh "docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:\${build_tag} ."
+ sh "docker push hub.gcloud.lab/rongqiyun/epaas:\${build_tag}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ stage('部署'){
+ stages('部署到容器云') {
+ stage('check') {
+ steps{
+ container('maven') {
+ script{
+ if (deploy_app == "true"){
+ def tmp = sh (
+ returnStdout: true,
+ script: "kubectl get deployment -n \${namespace} | grep \${JOB_NAME} | awk '{print \\$1}'"
+ )
+ //如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+ if(tmp.equals('')){
+ sh "helm init --client-only"
+ sh """helm repo add mychartmuseum http://xxxxxx \\
+ --username myuser \\
+ --password=mypass"""
+ sh """helm install --set name=\${JOB_NAME} \\
+ --set namespace=\${namespace} \\
+ --set deployment.image=\${image} \\
+ --set deployment.imagePullSecrets=\${harborProject} \\
+ --name \${namespace}-\${JOB_NAME} \\
+ mychartmuseum/soa-template"""
+ }else{
+ println "已经存在,替换镜像"
+ //epaas中一个pod的容器名称需要带上"-0"来区分
+ sh "kubectl set image deployment/\${JOB_NAME} \${JOB_NAME}-0=\${image} -n \${namespace}"
+ }
+ }else{
+ println "用户选择不部署代码"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
在jenkins x中查看:
jenkins blue ocean步骤日志:
云效中的日志:
triggers {
+ cron('H H * * *') //每天
+ }
+
pipeline中除了有对于时间的trigger,还支持了gitlab的触发,需要各种配置,不过如果真的对于gitlab的cicd有要求,直接使用gitlab-ci会更好,我们同时也对gitlab进行了runner的配置来支持gitlab的cicd。gitlab的cicd也提供了构建完后即销毁的过程。
功能最强大的过程莫过于自己使用pipeline脚本实现,选取最适合自己的,但是对于一个公司来说,如果要求业务方来掌握这些,特别是IT流动性大的时候,既需要重新培训,同个问题又会被问多遍,所以,只能将DevOps实现成一个图形化的东西,方便,简单,相对来说功能还算强大。
DevOps最难的可能都不是以上这些,关键是让用户接受,容器云最初推行时,公司原本传统的很多发版方式都需要进行改变,有些业务方不愿意改,或者有些代码把持久化的东西存到了代码中而不是分布式存储里,甚至有些用户方都不愿意维护老代码,看都不想看然后想上容器,一个公司在做技术架构的时候,过于混乱到最后填坑要么需要耗费太多精力甚至大换血。
最后,DevOps是云原生的必经之路!!!
文章同步:
博客园:https://www.cnblogs.com/w1570631036/p/11524673.html
个人网站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
gitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai
DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。
\\n公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。
","autoDesc":true}');export{e as data}; diff --git a/assets/elastic-spark.html-5RlCSmjx.js b/assets/elastic-spark.html-5RlCSmjx.js new file mode 100644 index 00000000..0b35437c --- /dev/null +++ b/assets/elastic-spark.html-5RlCSmjx.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-04e32b44","path":"/bigdata/spark/elastic-spark.html","title":"elastic spark","lang":"zh-CN","frontmatter":{"description":"elastic spark Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv (2)数据RDBMS:mysql、oracle、mssql (3)NOSQL数据库:HBase、ES、Redis (4)消息对象:Redis","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"elastic spark"}],["meta",{"property":"og:description","content":"elastic spark Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv (2)数据RDBMS:mysql、oracle、mssql (3)NOSQL数据库:HBase、ES、Redis (4)消息对象:Redis"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T11:42:16.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-25T11:42:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"elastic spark\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T11:42:16.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、原生RDD支持","slug":"一、原生rdd支持","link":"#一、原生rdd支持","children":[{"level":3,"title":"1.1 基础配置","slug":"_1-1-基础配置","link":"#_1-1-基础配置","children":[]},{"level":3,"title":"1.2 读取es数据","slug":"_1-2-读取es数据","link":"#_1-2-读取es数据","children":[]},{"level":3,"title":"1.3 写数据","slug":"_1-3-写数据","link":"#_1-3-写数据","children":[]}]},{"level":2,"title":"二、Spark Streaming","slug":"二、spark-streaming","link":"#二、spark-streaming","children":[]},{"level":2,"title":"三、Spark SQL","slug":"三、spark-sql","link":"#三、spark-sql","children":[]},{"level":2,"title":"四、Spark Structure Streaming","slug":"四、spark-structure-streaming","link":"#四、spark-structure-streaming","children":[]},{"level":2,"title":"五、Spark on kubernetes Operator","slug":"五、spark-on-kubernetes-operator","link":"#五、spark-on-kubernetes-operator","children":[]}],"git":{"createdTime":1706182936000,"updatedTime":1706182936000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":1.71,"words":514},"filePathRelative":"bigdata/spark/elastic-spark.md","localizedDate":"2024年1月25日","excerpt":"Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:\\n(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
\\n(2)数据RDBMS:mysql、oracle、mssql
\\n(3)NOSQL数据库:HBase、ES、Redis
\\n(4)消息对象:Redis
Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
(2)数据RDBMS:mysql、oracle、mssql
(3)NOSQL数据库:HBase、ES、Redis
(4)消息对象:Redis
elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。
相关库引入:
<dependency>
+ <groupId>org.elasticsearch</groupId>
+ <artifactId>elasticsearch-spark-30_2.13</artifactId>
+ <version>8.1.3</version>
+ </dependency>
+
public static SparkConf getSparkConf() {
+ SparkConf sparkConf = new SparkConf().setAppName("elasticsearch-spark-demo");
+ sparkConf.set("es.nodes", "host")
+ .set("es.port", "xxxxxx")
+ .set("es.nodes.wan.only", "true")
+ .set("es.net.http.auth.user", "elxxxxastic")
+ .set("es.net.http.auth.pass", "xxxx")
+ .setMaster("local[*]");
+ return sparkConf;
+}
+
这里用的是kibana提供的sample data里面的索引kibana_sample_data_ecommerce,也可以替换成自己的索引。
public static void main(String[] args) {
+ SparkConf conf = getSparkConf();
+ try (JavaSparkContext jsc = new JavaSparkContext(conf)) {
+
+ JavaPairRDD<String, Map<String, Object>> esRDD =
+ JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce");
+ esRDD.collect().forEach(System.out::println);
+ }
+}
+
esRDD同时也支持query语句esRDD(final JavaSparkContext jsc, final String resource, final String query),一般对es的查询都需要根据时间筛选一下,不过相对于es的官方sdk,并没有那么友好的api,只能直接使用原生的dsl语句。
支持序列化对象、json,并且能够使用占位符动态索引写入数据(使用较少),不过多介绍了。
public static void jsonWrite(){
+ String json1 = "{\\"reason\\" : \\"business\\",\\"airport\\" : \\"SFO\\"}";
+ String json2 = "{\\"participants\\" : 5,\\"airport\\" : \\"OTP\\"}";
+ JavaRDD<String> stringRDD = jsc.parallelize(ImmutableList.of(json1, json2));
+ JavaEsSpark.saveJsonToEs(stringRDD, "spark-json");
+}
+
比较常用的读写也就这些,更多可以看下官网相关介绍。
spark的实时处理,es5.0的时候开始支持,目前
直接用idea下载代码https://github.com/elastic/elasticsearch.git\\n
\\n切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好\\n
","autoDesc":true}');export{e as data}; diff --git "a/assets/elasticsearch\346\272\220\347\240\201debug.html-riyCnJZU.js" "b/assets/elasticsearch\346\272\220\347\240\201debug.html-riyCnJZU.js" new file mode 100644 index 00000000..edcc9d7d --- /dev/null +++ "b/assets/elasticsearch\346\272\220\347\240\201debug.html-riyCnJZU.js" @@ -0,0 +1 @@ +import{_ as e}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as a,c as i,e as s}from"./app-cS6i7hsH.js";const t={},r=s('直接用idea下载代码https://github.com/elastic/elasticsearch.git
切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好
为了方便, 在 gradle/run.gradle 中关闭 Auth 认证:
setting 'xpack.security.enabled', 'false'
或者使用其中的用户名密码:
user username: 'elastic-admin', password: 'elastic-password', role: 'superuser'
先启动上面的 remote debug, 然后用 gradlew 启动项目:
./gradlew :run --debug-jvm 打开浏览器http://localhost:9200即可看到es相关信息了
',12),h=[r];function c(d,l){return a(),i("div",null,h)}const o=e(t,[["render",c],["__file","elasticsearch源码debug.html.vue"]]);export{o as default}; diff --git "a/assets/gap\351\224\201.html-0qMpGSdo.js" "b/assets/gap\351\224\201.html-0qMpGSdo.js" new file mode 100644 index 00000000..4fb5781a --- /dev/null +++ "b/assets/gap\351\224\201.html-0qMpGSdo.js" @@ -0,0 +1 @@ +import{_ as a}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as t,c as o,a as e,d as r}from"./app-cS6i7hsH.js";const c={},s=e("h1",{id:"gap锁",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#gap锁","aria-hidden":"true"},"#"),r(" gap锁")],-1),_=[s];function n(d,i){return t(),o("div",null,_)}const p=a(c,[["render",n],["__file","gap锁.html.vue"]]);export{p as default}; diff --git "a/assets/gap\351\224\201.html-i4faTCf2.js" "b/assets/gap\351\224\201.html-i4faTCf2.js" new file mode 100644 index 00000000..463eedc1 --- /dev/null +++ "b/assets/gap\351\224\201.html-i4faTCf2.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3192e5a0","path":"/database/mysql/gap%E9%94%81.html","title":"gap锁","lang":"zh-CN","frontmatter":{"description":"gap锁","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/gap%E9%94%81.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"gap锁"}],["meta",{"property":"og:description","content":"gap锁"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T11:42:16.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-25T11:42:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"gap锁\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T11:42:16.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706092222000,"updatedTime":1706182936000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.01,"words":2},"filePathRelative":"database/mysql/gap锁.md","localizedDate":"2024年1月24日","excerpt":"留空
\\n","autoDesc":true}');export{t as data}; diff --git a/assets/index.html-vgYoWX9F.js b/assets/index.html-vgYoWX9F.js new file mode 100644 index 00000000..b5b4d971 --- /dev/null +++ b/assets/index.html-vgYoWX9F.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-71b3ae87","path":"/interview/","title":"Interview","lang":"zh-CN","frontmatter":{"title":"Interview","article":false,"feed":false,"sitemap":false,"description":"","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interview/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Interview"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"article:author","content":"Zephery"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Interview\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":"","autoDesc":true}');export{e as data}; diff --git a/assets/index.html-xgoJibcf.js b/assets/index.html-xgoJibcf.js new file mode 100644 index 00000000..a67d4303 --- /dev/null +++ b/assets/index.html-xgoJibcf.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-258f309e","path":"/open-source-project/","title":"","lang":"zh-CN","frontmatter":{"description":"jfaowejfoewj","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/open-source-project/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:description","content":"jfaowejfoewj"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T10:30:22.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T10:30:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T10:30:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706092222000,"updatedTime":1706092222000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0,"words":1},"filePathRelative":"open-source-project/README.md","localizedDate":"2024年1月24日","excerpt":"jfaowejfoewj
\\n","autoDesc":true}');export{e as data}; diff --git a/assets/index.html-yuZKtvix.js b/assets/index.html-yuZKtvix.js new file mode 100644 index 00000000..266ce1bb --- /dev/null +++ b/assets/index.html-yuZKtvix.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-14c69af4","path":"/java/","title":"Java","lang":"zh-CN","frontmatter":{"title":"Java","article":false,"feed":false,"sitemap":false,"description":"","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Java"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"article:author","content":"Zephery"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Java\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":"","autoDesc":true}');export{e as data}; diff --git a/assets/index.html-zEn3PTqR.js b/assets/index.html-zEn3PTqR.js new file mode 100644 index 00000000..80418a6c --- /dev/null +++ b/assets/index.html-zEn3PTqR.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-71fde78e","path":"/devops/","title":"三、DevOps","lang":"zh-CN","frontmatter":{"description":"三、DevOps DevOps DevOps平台 Gitlab-ci jenkins-x.md tekton","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/devops/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"三、DevOps"}],["meta",{"property":"og:description","content":"三、DevOps DevOps DevOps平台 Gitlab-ci jenkins-x.md tekton"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T03:25:58.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T03:25:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"三、DevOps\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T03:25:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706066758000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.08,"words":24},"filePathRelative":"devops/README.md","localizedDate":"2024年1月24日","excerpt":"1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。
2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。
3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②单机吞吐量:十万级,最大的优点,就是吞吐量高; ③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源; ④时效性:ms级; ⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用; ⑥消息可靠性:经过参数优化配置,消息可以做到0丢失; ⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。
缺点: ①由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序; ②有可能消息重复消费; ③依赖zookeeper进行元数据管理,等等。
4、为什么说Kafka性能很好,体现在哪里? ①顺序读写 ②零拷贝 ③分区 ④批量发送 ⑤数据压缩
5、请说明什么是传统的消息传递方法? 传统的消息传递方法包括两种: 排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。 发布-订阅:在这个模型中,消息被广播给所有的用户。
6、请说明Kafka相对传统技术有什么优势? ①快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。 ②可伸缩:在一组机器上对数据进行分区 ③和简化,以支持更大的数据 ④持久:消息是持久性的,并在集群中进 ⑤行复制,以防止数据丢失。 ⑥设计:它提供了容错保证和持久性
7、解释Kafka的Zookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka吗? Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。 不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。 Zookeeper主要用于在集群中不同节点之间进行通信 在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取 除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。
8、解释Kafka的用户如何消费信息? 在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。
9、解释如何提高远程用户的吞吐量? 如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。
10、解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息? 在数据中,为了精确地获得Kafka的消息,你必须遵循两件事: 在数据消耗期间避免重复,在数据生产过程中避免重复。 这里有两种方法,可以在数据生成时准确地获得一个语义: 每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功 在消息中包含一个主键(UUID或其他),并在用户中进行反复制
11、解释如何减少ISR中的扰动?broker什么时候离开ISR? ISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会从ISR中删除。
12、Kafka为什么需要复制? Kafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。
13、如果副本在ISR中停留了很长时间表明什么? 如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。
14、请说明如果首选的副本不在ISR中会发生什么? 如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。
15、有可能在生产后发生消息偏移吗? 在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。
作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。
16、Kafka的设计时什么样的呢? Kafka将消息以topic为单位进行归纳 将向Kafka topic发布消息的程序成为producers. 将预订topics并消费消息的程序成为consumer. Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker. producers通过网络将消息发送到Kafka集群,集群向消费者提供消息
17、数据传输的事物定义有哪三种? (1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输 (2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输. (3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的
18、Kafka判断一个节点是否还活着有那两个条件? (1)节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接 (2)如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久
19、producer是否直接将数据发送到broker的leader(主节点)? producer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发,为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。
20、Kafa consumer是否可以消费指定分区消息? Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的
21、Kafka消息是采用Pull模式,还是Push模式? Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式
Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略
Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发
22、Kafka存储在硬盘上的消息格式是什么? 消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。 消息长度: 4 bytes (value: 1+4+n) 版本号: 1 byte CRC校验码: 4 bytes 具体的消息: n bytes
23、Kafka高效文件存储设计特点: (1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。 (2).通过索引信息可以快速定位message和确定response的最大大小。 (3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。 (4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
24、Kafka 与传统消息系统之间有三个关键区别 (1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留 (2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性 (3).Kafka 支持实时的流式处理
25、Kafka创建Topic时如何将分区放置到不同的Broker中 副本因子不能大于 Broker 的个数; 第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的; 其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推; 剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的
26、Kafka新建的分区会在哪个目录下创建 在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。 但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
27、partition的数据如何保存到硬盘 topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增, 且消息有序 Partition文件下有多个segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名
28、kafka的ack机制 request.required.acks有三个值 0 1 -1 0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据 1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失 -1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失
29、Kafka的消费者如何消费数据 消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费。同时也可以按照指定的offset进行重新消费。
30、消费者负载均衡策略 结合consumer的加入和退出进行再平衡策略。
31、kafka消息数据是否有序? 消费者组里某具体分区是有序的,所以要保证有序只能建一个分区,但是实际这样会存在性能问题,具体业务具体分析后确认。
32、kafaka生产数据时数据的分组策略,生产者决定数据产生到集群的哪个partition中 每一条消息都是以(key,value)格式 Key是由生产者发送数据传入 所以生产者(key)决定了数据产生到集群的哪个partition
33、kafka consumer 什么情况会触发再平衡reblance? ①一旦消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中的所有消费者都要执行再平衡。 ②订阅主题分区发生变化,所有消费者也都要再平衡。
34、描述下kafka consumer 再平衡步骤? ①关闭数据拉取线程,情空队列和消息流,提交偏移量; ②释放分区所有权,删除zk中分区和消费者的所有者关系; ③将所有分区重新分配给每个消费者,每个消费者都会分到不同分区; ④将分区对应的消费者所有关系写入ZK,记录分区的所有权信息; ⑤重启消费者拉取线程管理器,管理每个分区的拉取线程。
35、Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么 36、Kafka中的HW、LEO、LSO、LW等分别代表什么? 37、Kafka中是怎么体现消息顺序性的? 38、Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么? 39、Kafka生产者客户端的整体结构是什么样子的? 40、Kafka生产者客户端中使用了几个线程来处理?分别是什么? 41、Kafka的旧版Scala的消费者客户端的设计有什么缺陷? 42、“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段? 43、消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1? 44、有哪些情形会造成重复消费? 45、那些情景下会造成消息漏消费? 46、KafkaConsumer是非线程安全的,那么怎么样实现多线程消费? 47、简述消费者与消费组之间的关系 48、当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑? 49、topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么? 50、topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么? 51、创建topic时如何选择合适的分区数? 52、Kafka目前有那些内部topic,它们都有什么特征?各自的作用又是什么? 53、优先副本是什么?它有什么特殊的作用? 54、Kafka有哪几处地方有分区分配的概念?简述大致的过程及原理 55、简述Kafka的日志目录结构 56、Kafka中有那些索引文件? 57、如果我指定了一个offset,Kafka怎么查找到对应的消息? 58、如果我指定了一个timestamp,Kafka怎么查找到对应的消息? 59、聊一聊你对Kafka的Log Retention的理解 60、聊一聊你对Kafka的Log Compaction的理解 61、聊一聊你对Kafka底层存储的理解(页缓存、内核层、块层、设备层) 62、聊一聊Kafka的延时操作的原理 63、聊一聊Kafka控制器的作用 64、消费再均衡的原理是什么?(提示:消费者协调器和消费组协调器) 65、Kafka中的幂等是怎么实现的 66、Kafka中的事务是怎么实现的(这题我去面试6家被问4次,照着答案念也要念十几分钟,面试官简直凑不要脸。实在记不住的话...只要简历上不写精通Kafka一般不会问到,我简历上写的是“熟悉Kafka,了解RabbitMQ....”) 67、Kafka中有那些地方需要选举?这些地方的选举策略又有哪些? 68、失效副本是指什么?有那些应对措施? 69、多副本下,各个副本中的HW和LEO的演变过程 70、为什么Kafka不支持读写分离? 71、Kafka在可靠性方面做了哪些改进?(HW, LeaderEpoch) 72、Kafka中怎么实现死信队列和重试队列? 73、Kafka中的延迟队列怎么实现(这题被问的比事务那题还要多!!!听说你会Kafka,那你说说延迟队列怎么实现?) 74、Kafka中怎么做消息审计? 75、Kafka中怎么做消息轨迹? 76、Kafka中有那些配置参数比较有意思?聊一聊你的看法 77、Kafka中有那些命名比较有意思?聊一聊你的看法 78、Kafka有哪些指标需要着重关注? 79、怎么计算Lag?(注意read_uncommitted和read_committed状态下的不同) 80、Kafka的那些设计让它有如此高的性能? 81、Kafka有什么优缺点? 82、还用过什么同质类的其它产品,与Kafka相比有什么优缺点? 83、为什么选择Kafka? 84、在使用Kafka的过程中遇到过什么困难?怎么解决的? 85、怎么样才能确保Kafka极大程度上的可靠性? 86、聊一聊你对Kafka生态的理解
',40),f=[p];function K(t,c){return e(),k("div",null,f)}const n=a(o,[["render",K],["__file","kafka.html.vue"]]);export{n as default}; diff --git a/assets/kafka.html-K4MxmLWF.js b/assets/kafka.html-K4MxmLWF.js new file mode 100644 index 00000000..e729904a --- /dev/null +++ b/assets/kafka.html-K4MxmLWF.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-d7d7df80","path":"/middleware/kafka/kafka.html","title":"kafka面试题","lang":"zh-CN","frontmatter":{"description":"kafka面试题 1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。 2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。 3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②单机吞吐量:十万级,最大的优点,就是吞吐量高; ③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源; ④时效性:ms级; ⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用; ⑥消息可靠性:经过参数优化配置,消息可以做到0丢失; ⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/kafka/kafka.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"kafka面试题"}],["meta",{"property":"og:description","content":"kafka面试题 1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。 2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。 3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②单机吞吐量:十万级,最大的优点,就是吞吐量高; ③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源; ④时效性:ms级; ⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用; ⑥消息可靠性:经过参数优化配置,消息可以做到0丢失; ⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T17:34:45.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-25T17:34:45.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"kafka面试题\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T17:34:45.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706204085000,"updatedTime":1706204085000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":17.01,"words":5103},"filePathRelative":"middleware/kafka/kafka.md","localizedDate":"2024年1月25日","excerpt":"1、请说明什么是Apache Kafka?\\nApache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。
\\n2、说说Kafka的使用场景?\\n①异步处理\\n②应用解耦\\n③流量削峰\\n④日志处理\\n⑤消息通讯等。
\\n3、使用Kafka有什么优点和缺点?\\n优点:\\n①支持跨数据中心的消息复制;\\n②单机吞吐量:十万级,最大的优点,就是吞吐量高;\\n③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源;\\n④时效性:ms级;\\n⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;\\n⑥消息可靠性:经过参数优化配置,消息可以做到0丢失;\\n⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。
","autoDesc":true}');export{a as data}; diff --git "a/assets/lucene\346\220\234\347\264\242\345\216\237\347\220\206.html-xLQYDnip.js" "b/assets/lucene\346\220\234\347\264\242\345\216\237\347\220\206.html-xLQYDnip.js" new file mode 100644 index 00000000..aa11d27e --- /dev/null +++ "b/assets/lucene\346\220\234\347\264\242\345\216\237\347\220\206.html-xLQYDnip.js" @@ -0,0 +1 @@ +import{_ as c}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as t,c as a,a as e,d as n}from"./app-cS6i7hsH.js";const o={},r=e("h1",{id:"lucene搜索原理",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#lucene搜索原理","aria-hidden":"true"},"#"),n(" lucene搜索原理")],-1),_=[r];function s(l,d){return t(),a("div",null,_)}const h=c(o,[["render",s],["__file","lucene搜索原理.html.vue"]]);export{h as default}; diff --git "a/assets/lucene\346\220\234\347\264\242\345\216\237\347\220\206.html-xqazIHll.js" "b/assets/lucene\346\220\234\347\264\242\345\216\237\347\220\206.html-xqazIHll.js" new file mode 100644 index 00000000..eca52cfc --- /dev/null +++ "b/assets/lucene\346\220\234\347\264\242\345\216\237\347\220\206.html-xqazIHll.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-478d0de6","path":"/java/lucene%E6%90%9C%E7%B4%A2%E5%8E%9F%E7%90%86.html","title":"lucene搜索原理","lang":"zh-CN","frontmatter":{"description":"lucene搜索原理","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/lucene%E6%90%9C%E7%B4%A2%E5%8E%9F%E7%90%86.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"lucene搜索原理"}],["meta",{"property":"og:description","content":"lucene搜索原理"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T03:25:58.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T03:25:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"lucene搜索原理\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T03:25:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706066758000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.02,"words":5},"filePathRelative":"java/lucene搜索原理.md","localizedDate":"2024年1月24日","excerpt":"从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。
步骤: (1)用户发送一个请求到nginx,nginx对请求进行分发。 (2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。 (3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。 (4)查询数据库的时候,mysql作了主主备份。
Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。
我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap来进行缓存。
(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。 (3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下: a.为每一个Mapper分配一个Cache缓存对象; b.多个Mapper共用一个Cache缓存对象;
在mybatis的配置文件中添加:
<settings>
+ <!--开启二级缓存-->
+ <setting name="cacheEnabled" value="true"/>
+</settings>
+
然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):
<cache eviction="LRU"
+ type="org.apache.ibatis.cache.impl.PerpetualCache"
+ flushInterval="120000"
+ size="1024"
+ readOnly="true"/>
+
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:
`,22),g=a("li",null,"MyBatis自身提供的缓存实现;",-1),m=a("li",null,"用户自定义的Cache接口实现;",-1),h=a("br",null,null,-1),v={href:"http://blog.csdn.net/xiadi934/article/details/50786293",target:"_blank",rel:"noopener noreferrer"},b={href:"http://blog.csdn.net/luanlouis/article/details/41390801",target:"_blank",rel:"noopener noreferrer"},y=e(`(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。 (3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。
下面将介绍使用Redis集中式缓存在个人网站的应用。
Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 **SpEL(Spring Expression Language)**来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。 下面是Spring Cache常用的注解:
(1)@Cacheable @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
(2)@CachePut @CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
(3)@CacheEvict @CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
allEntries | 是否清空所有缓存内容,默认为false | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false | @CachEvict(value=”testcache”,beforeInvocation=true) |
但是有个问题: Spring官方认为:缓存过期时间由各个产商决定,所以并不提供缓存过期时间的注解。所以,如果想实现各个元素过期时间不同,就需要自己重写一下Spring cache。
一般是Spring常用的包+Spring data redis的包,记得注意去掉所有冲突的包,之前才过坑,Spring-data-MongoDB已经有SpEL的库了,和自己新引进去的冲突,搞得我以为自己是配置配错了,真是个坑,注意,开发过程中一定要去除掉所有冲突的包!!!
需要启用缓存的注解开关,并配置好Redis。序列化方式也要带上,否则会碰到幽灵bug。
<!-- 启用缓存注解开关,此处可自定义keyGenerator -->
+ <cache:annotation-driven/>
+ <bean id="jedisConnectionFactory"
+ class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+ <property name="hostName" value="\${host}"/>
+ <property name="port" value="\${port}"/>
+ <property name="password" value="\${password}"/>
+ <property name="database" value="\${redis.default.db}"/>
+ <property name="timeout" value="\${timeout}"/>
+ <property name="poolConfig" ref="jedisPoolConfig"/>
+ <property name="usePool" value="true"/>
+ </bean>
+ <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+ <property name="connectionFactory" ref="jedisConnectionFactory"/>
+ <!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->
+ <property name="keySerializer">
+ <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
+ </property>
+ <property name="hashKeySerializer">
+ <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
+ </property>
+ <property name="valueSerializer">
+ <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
+ </property>
+ <property name="hashValueSerializer">
+ <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
+ </property>
+ </bean>
+ <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
+ <constructor-arg name="redisOperations" ref="redisTemplate" />
+ <!--统一过期时间-->
+ <property name="defaultExpiration" value="\${redis.defaultExpiration}"/>
+ </bean>
+
在分布式系统中,很容易存在不同类相同名字的方法,如A.getAll(),B.getAll(),默认的key(getAll)都是一样的,会很容易产生问题,所以,需要自定义key来实现分布式环境下的不同。
@Component("customKeyGenerator")
+public class CustomKeyGenerator implements KeyGenerator {
+ @Override
+ public Object generate(Object o, Method method, Object... objects) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(o.getClass().getName());
+ sb.append(".");
+ sb.append(method.getName());
+ for (Object obj : objects) {
+ sb.append(obj.toString());
+ }
+ return sb.toString();
+ }
+}
+
之后,存储的key就变为:com.myblog.service.impl.BlogServiceImpl.getBanner。
在所需要的方法上添加注解,比如,首页中的那几张幻灯片,每次进入首页都需要查询数据库,这里,我们直接放入缓存里,减少数据库的压力,还有就是那些热门文章,访问量比较大的,也放进数据库里。
@Override
+ @Cacheable(value = "getBanner", keyGenerator = "customKeyGenerator")
+ public List<Blog> getBanner() {
+ return blogMapper.getBanner();
+ }
+ @Override
+ @Cacheable(value = "getBlogDetail", key = "'blogid'.concat(#blogid)")
+ public Blog getBlogDetail(Integer blogid) {
+ Blog blog = blogMapper.selectByPrimaryKey(blogid);
+ if (blog == null) {
+ return null;
+ }
+ Category category = categoryMapper.selectByPrimaryKey(blog.getCategoryid());
+ blog.setCategory(category);
+ List<Tag> tags = tagMapper.getTagByBlogId(blog.getBlogid());
+ blog.setTags(tags.size() > 0 ? tags : null);
+ asyncService.updatebloghits(blogid);//异步更新阅读次数
+ logger.info("没有走缓存");
+ return blog;
+ }
+
我们调用一个getBlogDetail(获取博客详情)100次来对比一下时间。连接的数据库在深圳,本人在广州,还是有那么一丢丢的网路延时的。
public class SpringTest {
+ @Test
+ public void init() {
+ ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:spring-test.xml");
+ IBlogService blogService = (IBlogService) ctx.getBean("blogService");
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < 100; i++) {
+ blogService.getBlogDetail(615);
+ }
+ System.out.println(System.currentTimeMillis() - startTime);
+ }
+}
+
为了做一下对比,我们同时使用mybatis自身缓存来进行测试。
统计出结果如下:
没有使用任何缓存(mybatis一级缓存没有关闭):18305
+使用远程Redis缓存:12727
+使用Mybatis缓存:6649
+使用本地Redis缓存:5818
+
由结果看出,缓存的使用大大较少了获取数据的时间。
部署进个人博客之后,redis已经缓存的数据:
个人网站中共有两个栏目,一个是技术杂谈,另一个是生活笔记,每点击一次栏目的时候,会根据页数从数据库中查询数据,百度了下,大概有三种方法: (1)以页码作为Key,然后缓存整个页面。 (2)分条存取,只从数据库中获取分页的文章ID序列,然后从service(缓存策略在service中实现)中获取。 第一种,由于使用了第三方的插件PageHelper,分页获取的话会比较麻烦,同时整页缓存对内存压力也蛮大的,毕竟服务器只有2g。第二条实现方式简单,缺陷是依旧需要查询数据库,想了想还是放弃了。缓存的初衷是对请求频繁又不易变的数据,实际使用中很少会反复的请求同一页的数据(查询条件也相同),当然对数据中某些字段做缓存还是有必要的。
系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。\\n缓存常用语:\\n数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透\\n可查看Redis实战(一) 使用缓存合理性
","autoDesc":true}');export{e as data}; diff --git a/assets/serverlog.html-9M0lHfZ_.js b/assets/serverlog.html-9M0lHfZ_.js new file mode 100644 index 00000000..f31502da --- /dev/null +++ b/assets/serverlog.html-9M0lHfZ_.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-60eda44c","path":"/java/serverlog.html","title":"被挖矿攻击","lang":"zh-CN","frontmatter":{"description":"被挖矿攻击 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看: stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/serverlog.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"被挖矿攻击"}],["meta",{"property":"og:description","content":"被挖矿攻击 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看: stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T10:30:22.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T10:30:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"被挖矿攻击\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T10:30:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706092222000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.57,"words":172},"filePathRelative":"java/serverlog.md","localizedDate":"2024年1月24日","excerpt":"服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
\\n
\\nstratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG
服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG
使用top查看:
iptables -A INPUT -s xmr.crypto-pool.fr -j DROP
+iptables -A OUTPUT -d xmr.crypto-pool.fr -j DROP
+
19号添加mongodb之后,20号重启了服务器,但是忘记启动mongodb,导致后台一直在重连mongodb,也就导致了服务访问超级超级慢,记住要启动所需要的组件。
Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。
\\n使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。
\\nSpark Operator包括如下几个组件:
","autoDesc":true}');export{e as data}; diff --git a/assets/spark on k8s operator.html-oBat6Q5m.js b/assets/spark on k8s operator.html-oBat6Q5m.js new file mode 100644 index 00000000..07b4bf88 --- /dev/null +++ b/assets/spark on k8s operator.html-oBat6Q5m.js @@ -0,0 +1,73 @@ +import{_ as p}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as o,o as c,c as l,a as n,d as s,b as t,e}from"./app-cS6i7hsH.js";const i={},u=e(`Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。
使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。
Spark Operator包括如下几个组件:
安装
# 添加源
+helm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operator
+# 安装
+helm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace
+
这个时候如果直接执行kubectl apply -f examples/spark-pi.yaml调用样例,会出现以下报错,需要创建对象的serviceaccount。
forbidden: error looking up service account default/spark: serviceaccount \\"spark\\" not found.\\n\\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:568)\\n\\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport
+
同时也要安装rbac
`,10),r={href:"https://github.com/Zephery/spark-java-demo/blob/master/k8s/rbac.yaml",target:"_blank",rel:"noopener noreferrer"},k=e(`执行脚本,这里用的app.jar是一个读取es数据源,然后计算,为了展示效果,在程序的后面用了TimeUnit.HOURS.sleep(1)方便观察。
代码如下:
SparkConf sparkConf = new SparkConf().setAppName("Spark WordCount Application (java)");
+ sparkConf.set("es.nodes", "xxx")
+ .set("es.port", "8xx0")
+ .set("es.nodes.wan.only", "true")
+ .set("es.net.http.auth.user", "xxx")
+ .set("es.net.http.auth.pass", "xxx-xxx");
+ try (JavaSparkContext jsc = new JavaSparkContext(sparkConf)) {
+ JavaRDD<Map<String, Object>> esRDD = JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce").values();
+
+ System.out.println(esRDD.partitions().size());
+
+ esRDD.map(x -> x.get("customer_full_name"))
+ .countByValue()
+ .forEach((x, y) -> System.out.println(x + ":" + y));
+ TimeUnit.HOURS.sleep(1);
+
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
打包之后kubectl create -f k8s.yaml即可
apiVersion: "sparkoperator.k8s.io/v1beta2"
+kind: SparkApplication
+metadata:
+ name: spark-demo
+ namespace: default
+spec:
+ type: Scala
+ mode: cluster
+ image: fewfew-docker.pkg.coding.net/spark-java-demo/spark-java-demo-new/spark-java-demo:master-1d8c164bced70a1c66837ea5c0180c61dfb48ac3
+ imagePullPolicy: Always
+ mainClass: com.spark.es.demo.EsReadGroupByName
+ mainApplicationFile: "local:///opt/spark/examples/jars/app.jar"
+ sparkVersion: "3.1.1"
+ restartPolicy:
+ type: Never
+ driver:
+ # 用cores必须要大于等于1,这里用coreRequest
+ coreRequest: "100m"
+ coreLimit: "1200m"
+ memory: "512m"
+ labels:
+ version: 3.1.1
+ serviceAccount: sparkoperator
+ executor:
+ # 用cores必须要大于等于1,这里用coreRequest
+ coreRequest: "100m"
+ instances: 1
+ memory: "512m"
+ labels:
+ version: 3.1.1
+
spark-demo-driver 1/1 Running 0 2m30s
+spark-wordcount-application-java-0ac352810a9728e1-exec-1 1/1 Running 0 2m1s
+
同时会自动生成相应的service。
Spark on k8s operator大大减少了spark的部署与运维成本,用容器的调度来替换掉yarn,
Spark Operator的主要组件如下:
1、SparkApplication Controller : 用于监控并相应SparkApplication的相关对象的创建、更新和删除事件;
2、Submission Runner:用于接收Controller的提交指令,并通过spark-submit 来提交Spark作业到K8S集群并创建Driver Pod,driver正常运行之后将启动Executor Pod;
3、Spark Pod Monitor:实时监控Spark作业相关Pod(Driver、Executor)的运行状态,并将这些状态信息同步到Controller ;
4、Mutating Admission Webhook:可选模块,但是在Spark Operator中基本上所有的有关Spark pod在Kubernetes上的定制化功能都需要使用到该模块,因此建议将enableWebhook这个选项设置为true。
5、sparkctl: 基于Spark Operator的情况下可以不再使用kubectl来管理Spark作业了,而是采用Spark Operator专用的作业管理工具sparkctl,该工具功能较kubectl功能更为强大、方便易用。
apis:用户编写yaml时的解析
Batchscheduler:批处理的调度,提供了支持volcano的接口
Client:
leader的选举
electionCfg := leaderelection.LeaderElectionConfig{
+ Lock: resourceLock,
+ LeaseDuration: *leaderElectionLeaseDuration,
+ RenewDeadline: *leaderElectionRenewDeadline,
+ RetryPeriod: *leaderElectionRetryPeriod,
+ Callbacks: leaderelection.LeaderCallbacks{
+ OnStartedLeading: func(c context.Context) {
+ close(startCh)
+ },
+ OnStoppedLeading: func() {
+ close(stopCh)
+ },
+ },
+}
+
+elector, err := leaderelection.NewLeaderElector(electionCfg)
+
参考:
`,25),d={href:"https://blog.csdn.net/chengyinwu/article/details/121049750",target:"_blank",rel:"noopener noreferrer"},m={href:"https://www.cnblogs.com/zhangmingcheng/p/15846133.html",target:"_blank",rel:"noopener noreferrer"};function v(b,g){const a=o("ExternalLinkIcon");return c(),l("div",null,[u,n("p",null,[s("rbac的文件在manifest/spark-operator-install/spark-operator-rbac.yaml,需要把namespace全部改为需要运行的namespace,这里我们任务放在default命名空间下,所以全部改为default,源文件见"),n("a",r,[s("rbac.yaml"),t(a)])]),k,n("p",null,[s("1."),n("a",d,[s("k8s client-go中Leader选举实现"),t(a)])]),n("p",null,[s("2.["),n("a",m,[s("Kubernetes基于leaderelection选举策略实现组件高可用"),t(a)])])])}const f=p(i,[["render",v],["__file","spark on k8s operator.html.vue"]]);export{f as default}; diff --git a/assets/style-hJpTPrwX.css b/assets/style-hJpTPrwX.css new file mode 100644 index 00000000..91ee2078 --- /dev/null +++ b/assets/style-hJpTPrwX.css @@ -0,0 +1 @@ +html[data-theme=dark]{--text-color: #9e9e9e;--bg-color: #0d1117;--bg-color-secondary: #161b22;--bg-color-tertiary: #21262c;--border-color: #30363d;--box-shadow: #282a32;--card-shadow: rgba(0, 0, 0, .3);--black: #fff;--dark-grey: #999;--light-grey: #666;--white: #000;--grey3: #bbb;--grey12: #333;--grey14: #111;--bg-color-light: #161b22;--bg-color-back: #0d1117;--bg-color-float: #161b22;--bg-color-blur: rgba(13, 17, 23, .9);--bg-color-float-blur: rgba(22, 27, 34, .9);--text-color-light: #a8a8a8;--text-color-lighter: #b1b1b1;--text-color-bright: #c5c5c5;--border-color-light: #2e333a;--border-color-dark: #394048}:root{--theme-color: #2980b9;--text-color: #2c3e50;--bg-color: #fff;--bg-color-secondary: #f8f8f8;--bg-color-tertiary: #efeef4;--border-color: #eaecef;--box-shadow: #f0f1f2;--card-shadow: rgba(0, 0, 0, .15);--black: #000;--dark-grey: #666;--light-grey: #999;--white: #fff;--grey3: #333;--grey12: #bbb;--grey14: #eee;--navbar-height: 3.75rem;--navbar-horizontal-padding: 1.5rem;--navbar-vertical-padding: .7rem;--navbar-mobile-height: 3.25rem;--navbar-mobile-horizontal-padding: 1rem;--navbar-mobile-vertical-padding: .5rem;--sidebar-width: 20rem;--sidebar-mobile-width: 16rem;--content-width: 780px;--home-page-width: 1160px;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", STHeiti, "Microsoft YaHei", SimSun, sans-serif;--font-family-heading: Georgia Pro, Crimson, Georgia, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", STHeiti, "Microsoft YaHei", SimSun, sans-serif;--font-family-mono: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;--line-numbers-width: 2.5rem;--color-transition: .3s ease;--transform-transition: .3s ease;--vp-bg: var(--bg-color);--vp-bgl: var(--bg-color-light);--vp-bglt: var(--bg-color-tertiary);--vp-c: var(--text-color);--vp-cl: var(--text-color-light);--vp-clt: var(--text-color-lighter);--vp-brc: var(--border-color);--vp-brcd: var(--border-color-dark);--vp-tc: var(--theme-color);--vp-tcl: var(--theme-color-light);--vp-ct: var(--color-transition);--vp-tt: var(--transform-transition);--bg-color-light: #fff;--bg-color-back: #f8f8f8;--bg-color-float: #fff;--bg-color-blur: rgba(255, 255, 255, .9);--bg-color-float-blur: rgba(255, 255, 255, .9);--text-color-light: #3a5169;--text-color-lighter: #476582;--text-color-bright: #6a8bad;--border-color-light: #eceef1;--border-color-dark: #cfd4db;--theme-color-dark: #2573a7;--theme-color-light: #2e90d0;--theme-color-mask: rgba(41, 128, 185, .15)}:root{--badge-tip-color: #42b983;--badge-warning-color: #f4cd00;--badge-danger-color: #f55;--badge-info-color: #0295ff;--badge-note-color: #666}.vp-badge{display:inline-block;vertical-align:center;height:18px;padding:0 6px;border-radius:3px;background:var(--vp-tc);color:var(--white);font-size:14px;line-height:18px;transition:background var(--vp-ct),color var(--vp-ct)}.vp-badge+.vp-badge{margin-inline-start:5px}h1 .vp-badge,h2 .vp-badge,h3 .vp-badge,h4 .vp-badge,h5 .vp-badge,h6 .vp-badge{vertical-align:top}.vp-badge.tip{background:var(--badge-tip-color)}.vp-badge.warning{background:var(--badge-warning-color)}.vp-badge.danger{background:var(--badge-danger-color)}.vp-badge.info{background:var(--badge-info-color)}.vp-badge.note{background:var(--badge-note-color)}.font-icon{display:inline-block}.theme-hope-content .font-icon{vertical-align:middle}:root{--balloon-border-radius: 2px;--balloon-color: rgba(16, 16, 16, .95);--balloon-text-color: #fff;--balloon-font-size: 12px;--balloon-move: 4px}button[aria-label][data-balloon-pos]{overflow:visible}[aria-label][data-balloon-pos]{position:relative;cursor:pointer}[aria-label][data-balloon-pos]:after{opacity:0;pointer-events:none;transition:all .18s ease-out .18s;text-indent:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-weight:400;font-style:normal;text-shadow:none;font-size:var(--balloon-font-size);background:var(--balloon-color);border-radius:2px;color:var(--balloon-text-color);border-radius:var(--balloon-border-radius);content:attr(aria-label);padding:.5em 1em;position:absolute;white-space:nowrap;z-index:10}[aria-label][data-balloon-pos]:before{width:0;height:0;border:5px solid transparent;border-top-color:var(--balloon-color);opacity:0;pointer-events:none;transition:all .18s ease-out .18s;content:"";position:absolute;z-index:10}[aria-label][data-balloon-pos]:hover:before,[aria-label][data-balloon-pos]:hover:after,[aria-label][data-balloon-pos][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-visible]:after,[aria-label][data-balloon-pos]:not([data-balloon-nofocus]):focus:before,[aria-label][data-balloon-pos]:not([data-balloon-nofocus]):focus:after{opacity:1;pointer-events:none}[aria-label][data-balloon-pos].font-awesome:after{font-family:FontAwesome,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif}[aria-label][data-balloon-pos][data-balloon-break]:after{white-space:pre}[aria-label][data-balloon-pos][data-balloon-break][data-balloon-length]:after{white-space:pre-line;word-break:break-word}[aria-label][data-balloon-pos][data-balloon-blunt]:before,[aria-label][data-balloon-pos][data-balloon-blunt]:after{transition:none}[aria-label][data-balloon-pos][data-balloon-pos=up]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=up][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos=down]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=down][data-balloon-visible]:after{transform:translate(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=up]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=up][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos=down]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=down][data-balloon-visible]:before{transform:translate(-50%)}[aria-label][data-balloon-pos][data-balloon-pos*=-left]:after{left:0}[aria-label][data-balloon-pos][data-balloon-pos*=-left]:before{left:5px}[aria-label][data-balloon-pos][data-balloon-pos*=-right]:after{right:0}[aria-label][data-balloon-pos][data-balloon-pos*=-right]:before{right:5px}[aria-label][data-balloon-pos][data-balloon-po*=-left]:hover:after,[aria-label][data-balloon-pos][data-balloon-po*=-left][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos*=-right]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos*=-right][data-balloon-visible]:after{transform:translate(0)}[aria-label][data-balloon-pos][data-balloon-po*=-left]:hover:before,[aria-label][data-balloon-pos][data-balloon-po*=-left][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos*=-right]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos*=-right][data-balloon-visible]:before{transform:translate(0)}[aria-label][data-balloon-pos][data-balloon-pos^=up]:before,[aria-label][data-balloon-pos][data-balloon-pos^=up]:after{bottom:100%;transform-origin:top;transform:translateY(var(--balloon-move))}[aria-label][data-balloon-pos][data-balloon-pos^=up]:after{margin-bottom:10px}[aria-label][data-balloon-pos][data-balloon-pos=up]:before,[aria-label][data-balloon-pos][data-balloon-pos=up]:after{left:50%;transform:translate(-50%,var(--balloon-move))}[aria-label][data-balloon-pos][data-balloon-pos^=down]:before,[aria-label][data-balloon-pos][data-balloon-pos^=down]:after{top:100%;transform:translateY(calc(var(--balloon-move) * -1))}[aria-label][data-balloon-pos][data-balloon-pos^=down]:after{margin-top:10px}[aria-label][data-balloon-pos][data-balloon-pos^=down]:before{width:0;height:0;border:5px solid transparent;border-bottom-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-pos=down]:after,[aria-label][data-balloon-pos][data-balloon-pos=down]:before{left:50%;transform:translate(-50%,calc(var(--balloon-move) * -1))}[aria-label][data-balloon-pos][data-balloon-pos=left]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=left][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos=right]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=right][data-balloon-visible]:after{transform:translateY(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=left][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos=right]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=right][data-balloon-visible]:before{transform:translateY(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:after,[aria-label][data-balloon-pos][data-balloon-pos=left]:before{right:100%;top:50%;transform:translate(var(--balloon-move),-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:after{margin-right:10px}[aria-label][data-balloon-pos][data-balloon-pos=left]:before{width:0;height:0;border:5px solid transparent;border-left-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-pos=right]:after,[aria-label][data-balloon-pos][data-balloon-pos=right]:before{left:100%;top:50%;transform:translate(calc(var(--balloon-move) * -1),-50%)}[aria-label][data-balloon-pos][data-balloon-pos=right]:after{margin-left:10px}[aria-label][data-balloon-pos][data-balloon-pos=right]:before{width:0;height:0;border:5px solid transparent;border-right-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-length]:after{white-space:normal}[aria-label][data-balloon-pos][data-balloon-length=small]:after{width:80px}[aria-label][data-balloon-pos][data-balloon-length=medium]:after{width:150px}[aria-label][data-balloon-pos][data-balloon-length=large]:after{width:260px}[aria-label][data-balloon-pos][data-balloon-length=xlarge]:after{width:380px}@media screen and (max-width: 768px){[aria-label][data-balloon-pos][data-balloon-length=xlarge]:after{width:90vw}}[aria-label][data-balloon-pos][data-balloon-length=fit]:after{width:100%}.vp-back-to-top-button{border-width:0;background:transparent;cursor:pointer;position:fixed!important;bottom:64px;inset-inline-end:1rem;z-index:100;width:48px;height:48px;padding:8px;border-radius:50%;background:var(--vp-bg);color:var(--vp-tc);box-shadow:2px 2px 10px 4px var(--card-shadow);transition:background var(--vp-ct),color var(--vp-ct),box-shadow var(--vp-ct)}@media (max-width: 719px){.vp-back-to-top-button{width:36px;height:36px}}@media print{.vp-back-to-top-button{display:none}}.vp-back-to-top-button:hover{color:var(--vp-tcl)}.vp-back-to-top-button .back-to-top-icon{overflow:hidden;width:100%;border-radius:50%;fill:currentcolor}.vp-scroll-progress{position:absolute;right:-2px;bottom:-2px;width:52px;height:52px}@media (max-width: 719px){.vp-scroll-progress{width:40px;height:40px}}.vp-scroll-progress svg{width:100%;height:100%}.vp-scroll-progress circle{opacity:.9;fill:none;stroke:var(--vp-tc);transform:rotate(-90deg);transform-origin:50% 50%;r:22;stroke-dasharray:0% 314.1593%;stroke-width:3px}@media (max-width: 719px){.vp-scroll-progress circle{r:18}}.fade-enter-active,.fade-leave-active{transition:opacity var(--vp-ct)}.fade-enter-from,.fade-leave-to{opacity:0}:root{--notice-width: 250px}.notice-fade-enter-active,.notice-fade-leave-active{transition:opacity .5s}.notice-fade-enter,.notice-fade-leave-to{opacity:0}.vp-notice-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1499;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media print{.vp-notice-mask{display:none}}.vp-notice-wrapper{position:fixed;top:80px;inset-inline-end:20px;z-index:1500;overflow:hidden;width:var(--notice-width);border-radius:8px;background:var(--vp-bg);box-shadow:0 2px 6px 0 var(--card-shadow)}@media print{.vp-notice-wrapper{display:none}}.vp-notice-wrapper.fullscreen{top:50vh;right:50vw;left:unset;transform:translate(50%,-50%)}.vp-notice-title{position:relative;margin:0;padding:8px 12px;background:var(--vp-tc);color:var(--white);font-weight:500;text-align:start}.vp-notice-title .close-icon{vertical-align:middle;float:right;width:1em;height:1em;margin:auto;padding:4px;border-radius:50%;background-color:#0003;color:var(--white);cursor:pointer}html[dir=rtl] .vp-notice-title .close-icon{float:left}.vp-notice-title .close-icon:hover{background-color:#0000004d}.vp-notice-content{margin:1rem .75rem;font-size:14px;line-height:1.5}.vp-notice-footer{padding-bottom:8px;text-align:center}.vp-notice-footer-action{display:inline-block;margin:4px;padding:8px 12px;border:none;border-radius:8px;background-color:var(--vp-bglt);color:var(--vp-c);cursor:pointer}.vp-notice-footer-action:hover{background-color:var(--vp-bgl)}.vp-notice-footer-action.primary{background-color:var(--vp-tc);color:var(--white)}.vp-notice-footer-action.primary:hover{background-color:var(--vp-tcl)}@media screen{.sr-only{position:absolute;overflow:hidden;clip:rect 0,0,0,0;width:1px;height:1px;margin:-1px;padding:0;border:0}}@media print{.sr-only{display:none}}.vp-catalog-wrapper{margin-top:8px;margin-bottom:8px}.vp-catalog-wrapper.index ol{padding-inline-start:0}.vp-catalog-wrapper.index li{list-style-type:none}.vp-catalog-wrapper.index .vp-catalogs{padding-inline-start:0}.vp-catalog-wrapper.index .vp-catalog{list-style-type:none}.vp-catalog-wrapper.index .vp-catalog-title:before{content:"§" counter(catalog-item,upper-roman) " "}.vp-catalog-wrapper.index .vp-child-catalogs{counter-reset:child-catalog}.vp-catalog-wrapper.index .vp-child-catalog{counter-increment:child-catalog}.vp-catalog-wrapper.index .vp-child-catalog .vp-catalog-title:before{content:counter(catalog-item) "." counter(child-catalog) " "}.vp-catalog-wrapper.index .vp-sub-catalogs{padding-inline-start:.5rem}.vp-catalogs{margin:0;counter-reset:catalog-item}.vp-catalogs.deep{padding-inline-start:0}.vp-catalogs.deep .vp-catalog{list-style-type:none}.vp-catalogs .font-icon{vertical-align:baseline;margin-inline-end:.25rem}.vp-catalog{counter-increment:catalog-item}.vp-catalog-main-title{margin-top:calc(.5rem - var(--navbar-height, 3.6rem));margin-bottom:.5rem;padding-top:var(--navbar-height, 3.6rem);font-weight:500;font-size:1.75rem}.vp-catalog-main-title:first-child{margin-bottom:.5rem!important}.vp-catalog-main-title:only-child{margin-bottom:0!important}.vp-catalog-child-title{margin-bottom:.5rem!important}.vp-catalog-child-title.has-children{margin-top:calc(.5rem - var(--navbar-height, 3.6rem));padding-top:var(--navbar-height, 3.6rem);border-bottom:1px solid var(--vp-brc);font-weight:500;font-size:1.3rem;transition:border-color var(--vp-ct)}.vp-catalog-child-title.has-children:only-child{margin-bottom:0!important}.vp-catalog-sub-title{font-weight:500;font-size:1.1rem}.vp-catalog-sub-title:only-child{margin-bottom:0!important}.vp-catalog-title{color:inherit;text-decoration:none}.vp-catalog-title:hover{color:var(--vp-tc)}.vp-child-catalogs{margin:0}.vp-child-catalog{list-style-type:disc}.vp-sub-catalogs{counter-reset:sub-catalog}.vp-sub-catalog{counter-increment:sub-catalog}.vp-sub-catalog .vp-link:before{content:counter(catalog-item) "." counter(child-catalog) "." counter(sub-catalog) " "}.vp-sub-catalogs-wrapper{display:flex;flex-wrap:wrap}.vp-sub-catalog-link{display:inline-block;margin:4px 8px;padding:4px 8px;border-radius:6px;background-color:var(--vp-bgl);line-height:1.5;overflow-wrap:break-word;transition:background-color var(--vp-ct),color var(--vp-ct)}.vp-sub-catalog-link:hover{background-color:var(--vp-tcl);color:var(--vp-bg);text-decoration:none!important}.vp-empty-catalog{font-size:1.25rem;text-align:center}:root{--external-link-icon-color: #aaa}.external-link-icon{position:relative;display:inline-block;color:var(--external-link-icon-color);vertical-align:middle;top:-1px}@media print{.external-link-icon{display:none}}.external-link-icon-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--nprogress-color: #29d;--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{background:var(--nprogress-color);position:fixed;z-index:var(--nprogress-z-index);top:0;left:0;width:100%;height:2px}:root{--copy-icon: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");--copied-icon: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E")}div[class*=language-]>button.copy-code-button{border-width:0;background:transparent;position:absolute;outline:none;cursor:pointer}@media print{div[class*=language-]>button.copy-code-button{display:none}}div[class*=language-]>button.copy-code-button .copy-icon{background:currentcolor;-webkit-mask-image:var(--copy-icon);mask-image:var(--copy-icon);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:1em;mask-size:1em}div[class*=language-]>button.copy-code-button:not(.fancy){border-width:0;background:transparent;cursor:pointer;position:absolute;top:.5em;right:.5em;z-index:5;width:2.5rem;height:2.5rem;padding:0;border-radius:.5rem;opacity:0;transition:opacity .4s}div[class*=language-]>button.copy-code-button:not(.fancy):hover,div[class*=language-]>button.copy-code-button:not(.fancy).copied{background:var(--code-hl-bg-color, rgba(0, 0, 0, .66))}div[class*=language-]>button.copy-code-button:not(.fancy):focus,div[class*=language-]>button.copy-code-button:not(.fancy).copied{opacity:1}div[class*=language-]>button.copy-code-button:not(.fancy).copied:after{content:attr(data-copied);position:absolute;top:0;right:calc(100% + .25rem);display:block;height:1.25rem;padding:.625rem;border-radius:.5rem;background:var(--code-hl-bg-color, rgba(0, 0, 0, .66));color:var(--code-ln-color, #9e9e9e);font-weight:500;line-height:1.25rem;white-space:nowrap}div[class*=language-]>button.copy-code-button:not(.fancy) .copy-icon{width:1.25rem;height:1.25rem;padding:.625rem;color:var(--code-ln-color, #9e9e9e);font-size:1.25rem}div[class*=language-]>button.copy-code-button.fancy{right:-14px;bottom:-14px;z-index:5;width:2rem;height:2rem;padding:7px 8px;border-radius:50%;background:#339af0;color:#fff}@media (max-width: 419px){div[class*=language-]>button.copy-code-button.fancy{right:0;bottom:0;width:28px;height:28px;border-radius:50% 10% 0}}div[class*=language-]>button.copy-code-button.fancy:hover{background:#228be6}div[class*=language-]>button.copy-code-button.fancy .copy-icon{width:100%;height:100%;color:#fff;font-size:1.25rem}@media (max-width: 419px){div[class*=language-]>button.copy-code-button.fancy .copy-icon{position:relative;top:2px;left:2px}}div[class*=language-]>button.copy-code-button.copied .copy-icon{-webkit-mask-image:var(--copied-icon);mask-image:var(--copied-icon)}div[class*=language-]:hover:before{display:none}div[class*=language-]:hover>button.copy-code-button:not(.fancy){opacity:1}/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */.pswp{--pswp-bg: #000;--pswp-placeholder-bg: #222;--pswp-root-z-index: 100000;--pswp-preloader-color: rgba(79, 79, 79, .4);--pswp-preloader-color-secondary: rgba(255, 255, 255, .9);--pswp-icon-color: #fff;--pswp-icon-color-secondary: #4f4f4f;--pswp-icon-stroke-color: #4f4f4f;--pswp-icon-stroke-width: 2px;--pswp-error-text-color: var(--pswp-icon-color)}.pswp{position:fixed;top:0;left:0;width:100%;height:100%;z-index:var(--pswp-root-z-index);display:none;touch-action:none;outline:0;opacity:.003;contain:layout style size;-webkit-tap-highlight-color:rgba(0,0,0,0)}.pswp:focus{outline:0}.pswp *{box-sizing:border-box}.pswp img{max-width:none}.pswp--open{display:block}.pswp,.pswp__bg{transform:translateZ(0);will-change:opacity}.pswp__bg{opacity:.005;background:var(--pswp-bg)}.pswp,.pswp__scroll-wrap{overflow:hidden}.pswp__scroll-wrap,.pswp__bg,.pswp__container,.pswp__item,.pswp__content,.pswp__img,.pswp__zoom-wrap{position:absolute;top:0;left:0;width:100%;height:100%}.pswp__img,.pswp__zoom-wrap{width:auto;height:auto}.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img{cursor:zoom-in}.pswp--click-to-zoom.pswp--zoomed-in .pswp__img{cursor:move;cursor:grab}.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active{cursor:grabbing}.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,.pswp__img{cursor:zoom-out}.pswp__container,.pswp__img,.pswp__button,.pswp__counter{-webkit-user-select:none;-moz-user-select:none;user-select:none}.pswp__item{z-index:1;overflow:hidden}.pswp__hidden{display:none!important}.pswp__content{pointer-events:none}.pswp__content>*{pointer-events:auto}.pswp__error-msg-container{display:grid}.pswp__error-msg{margin:auto;font-size:1em;line-height:1;color:var(--pswp-error-text-color)}.pswp .pswp__hide-on-close{opacity:.005;will-change:opacity;transition:opacity var(--pswp-transition-duration) cubic-bezier(.4,0,.22,1);z-index:10;pointer-events:none}.pswp--ui-visible .pswp__hide-on-close{opacity:1;pointer-events:auto}.pswp__button{position:relative;display:block;width:50px;height:60px;padding:0;margin:0;overflow:hidden;cursor:pointer;background:none;border:0;box-shadow:none;opacity:.85;-webkit-appearance:none;-webkit-touch-callout:none}.pswp__button:hover,.pswp__button:active,.pswp__button:focus{transition:none;padding:0;background:none;border:0;box-shadow:none;opacity:1}.pswp__button:disabled{opacity:.3;cursor:auto}.pswp__icn{fill:var(--pswp-icon-color);color:var(--pswp-icon-color-secondary)}.pswp__icn{position:absolute;top:14px;left:9px;width:32px;height:32px;overflow:hidden;pointer-events:none}.pswp__icn-shadow{stroke:var(--pswp-icon-stroke-color);stroke-width:var(--pswp-icon-stroke-width);fill:none}.pswp__icn:focus{outline:0}div.pswp__img--placeholder,.pswp__img--with-bg{background:var(--pswp-placeholder-bg)}.pswp__top-bar{position:absolute;left:0;top:0;width:100%;height:60px;display:flex;flex-direction:row;justify-content:flex-end;z-index:10;pointer-events:none!important}.pswp__top-bar>*{pointer-events:auto;will-change:opacity}.pswp__button--close{margin-right:6px}.pswp__button--arrow{position:absolute;width:75px;height:100px;top:50%;margin-top:-50px}.pswp__button--arrow:disabled{display:none;cursor:default}.pswp__button--arrow .pswp__icn{top:50%;margin-top:-30px;width:60px;height:60px;background:none;border-radius:0}.pswp--one-slide .pswp__button--arrow{display:none}.pswp--touch .pswp__button--arrow{visibility:hidden}.pswp--has_mouse .pswp__button--arrow{visibility:visible}.pswp__button--arrow--prev{right:auto;left:0}.pswp__button--arrow--next{right:0}.pswp__button--arrow--next .pswp__icn{left:auto;right:14px;transform:scaleX(-1)}.pswp__button--zoom{display:none}.pswp--zoom-allowed .pswp__button--zoom{display:block}.pswp--zoomed-in .pswp__zoom-icn-bar-v{display:none}.pswp__preloader{position:relative;overflow:hidden;width:50px;height:60px;margin-right:auto}.pswp__preloader .pswp__icn{opacity:0;transition:opacity .2s linear;animation:pswp-clockwise .6s linear infinite}.pswp__preloader--active .pswp__icn{opacity:.85}@keyframes pswp-clockwise{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pswp__counter{height:30px;margin-top:15px;margin-inline-start:20px;font-size:14px;line-height:30px;color:var(--pswp-icon-color);text-shadow:1px 1px 3px var(--pswp-icon-color-secondary);opacity:.85}.pswp--one-slide .pswp__counter{display:none}.photo-swipe-loading{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center}.photo-swipe-bullets-indicator{position:absolute;bottom:30px;left:50%;display:flex;flex-direction:row;align-items:center;transform:translate(-50%)}.photo-swipe-bullet{width:12px;height:6px;margin:0 5px;border-radius:3px;background:#fff;transition:width var(--vp-tt),color var(--vp-ct)}.photo-swipe-bullet.active{width:30px;background:var(--vp-tc)}@keyframes message-move-in{0%{opacity:0;transform:translateY(-100%)}to{opacity:1;transform:translateY(0)}}@keyframes message-move-out{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-100%)}}#message-container{position:fixed;inset:calc(var(--navbar-height, 3.6rem) + 1rem) 0 auto;z-index:75;text-align:center}#message-container .message{display:inline-block;padding:8px 10px;border-radius:3px;background:var(--vp-bg);color:var(--vp-c);box-shadow:0 0 10px 0 var(--box-shadow, #f0f1f2);font-size:14px}#message-container .message.move-in{animation:message-move-in .3s ease-in-out}#message-container .message.move-out{animation:message-move-out .3s ease-in-out;animation-fill-mode:forwards}#message-container .message svg{position:relative;bottom:-.125em;margin-inline-end:5px}.vp-article-wrapper{position:relative;box-sizing:border-box;width:100%;margin:0 auto 1.25rem;text-align:start;overflow-wrap:break-word}@media (max-width: 959px){.vp-article-wrapper{margin:0 auto 1rem}}.vp-article-wrapper:last-child{margin-bottom:0}.vp-article-item{display:block;padding:.75rem 1.25rem;border-radius:.4rem;background:var(--bg-color-float);color:inherit;box-shadow:0 1px 3px 1px var(--card-shadow);transition:background var(--color-transition),box-shadow var(--color-transition)}@media (max-width: 959px){.vp-article-item{padding:.75rem 1rem}}@media (max-width: 419px){.vp-article-item{border-radius:0}}.vp-article-item:hover{box-shadow:0 2px 6px 2px var(--card-shadow)}.vp-article-item .sticky-icon{position:absolute;top:0;inset-inline-end:0;width:1.5rem;height:1.5rem;color:var(--theme-color)}.vp-article-item .page-info>span{display:flex;flex-shrink:0;align-items:center;margin-inline-end:.5em;line-height:1.8}.vp-article-item .page-info>span:after{--balloon-font-size: 8px;padding:.3em .6em!important}.vp-article-hr{margin-block:.375em .375em}.vp-article-title{position:relative;display:inline-block;color:var(--text-color);font-size:1.25rem;font-family:var(--font-family-heading);line-height:1.6;cursor:pointer}.vp-article-title:after{content:"";position:absolute;inset:auto 0 0;height:2px;background:var(--theme-color);visibility:hidden;transition:transform .3s ease-in-out;transform:scaleX(0)}.vp-article-title:hover{cursor:pointer}.vp-article-title:hover:after{visibility:visible;transform:scaleX(1)}.vp-article-title a{color:inherit;font-weight:600}.vp-article-title .lock-icon,.vp-article-title .slides-icon{position:relative;bottom:-.125em;display:inline-block;vertical-align:baseline;width:1em;height:1em;margin-inline-end:.25em;color:var(--theme-color)}.vp-article-title>span{word-break:break-word}.vp-article-cover{width:calc(100% + 2.5rem);margin:-.75rem -1.25rem .75rem;border-top-left-radius:.4rem;border-top-right-radius:.4rem}@media (max-width: 959px){.vp-article-cover{width:calc(100% + 2rem);margin:-.75rem -1rem .75rem}}@media (max-width: 419px){.vp-article-cover{border-radius:0}}.vp-article-excerpt{overflow:hidden;line-height:1.6;cursor:default}@media (max-width: 959px){.vp-article-excerpt{font-size:15px}}@media (max-width: 419px){.vp-article-excerpt{font-size:14px}}.vp-article-excerpt h1{display:none}.vp-article-excerpt h2{font-size:1.4em}.vp-article-excerpt h3{font-size:1.2em}.vp-article-excerpt h1,.vp-article-excerpt h2,.vp-article-excerpt h3,.vp-article-excerpt h4,.vp-article-excerpt h5,.vp-article-excerpt h6{margin-top:.5em;margin-bottom:.5em}.vp-article-excerpt h1+p{margin-top:.5em}.vp-article-excerpt p:first-child{margin-top:.5em}.vp-article-excerpt p:last-child{margin-bottom:.5em}.vp-article-excerpt div[class*=language-]{overflow:auto hidden}.vp-article-excerpt div[class*=language-] pre{margin:.85rem 0;line-height:1.375}.vp-article-excerpt div[class*=language-] pre code{padding:0;background:transparent}.vp-article-excerpt div[class*=language-].line-numbers-mode .line-numbers{padding:.85rem 0}.vp-article-excerpt .code-demo-wrapper,.vp-article-excerpt .external-link-icon,.vp-article-excerpt .footnote-anchor{display:none}.vp-article-excerpt section.footnotes{display:none}.vp-article-excerpt img{max-width:100%}.vp-article-excerpt figure{display:flex;flex-direction:column;width:auto;margin:1rem auto;text-align:center;transition:transform var(--transform-transition)}.vp-article-excerpt figure img{overflow:hidden;margin:0 auto;border-radius:8px}.vp-article-excerpt figure figcaption{display:inline-block;margin:6px auto;font-size:.8rem}.vp-article-excerpt figure figcaption:only-child{display:none}.vp-article-list{margin-top:calc(-.5rem - var(--navbar-height));padding-top:calc(var(--navbar-height) + .5rem);text-align:center}.vp-article-list:first-child{margin-top:calc(0rem - var(--navbar-height))}.vp-article-list .empty{max-width:560px;margin:0 auto;text-align:center}.vp-article-type-wrapper{position:relative;z-index:2;display:flex;align-items:center;justify-content:center;padding-inline-start:0;list-style:none;font-weight:600;font-size:18px}@media (max-width: 419px){.vp-article-type-wrapper{font-size:16px}}.vp-article-type{position:relative;vertical-align:middle;margin:.3em .8em;line-height:1.2;cursor:pointer}.vp-article-type:after{content:" ";position:absolute;inset:auto 50% -6px;height:2px;border-radius:1px;background:var(--theme-color);visibility:hidden;transition:inset .2s ease-in-out}.vp-article-type a{display:inline-block;color:inherit;transition:all .3s ease-in-out}.vp-article-type.active{position:relative}.vp-article-type.active a{color:var(--theme-color);transform:scale(1.1)}.vp-article-type:hover:after,.vp-article-type.active:after{inset:auto calc(50% - 8px) -6px;visibility:visible}.vp-blog-hero{position:relative;display:flex;flex-direction:column;justify-content:center;height:450px;margin-bottom:1rem;color:#eee;font-family:var(--font-family-heading)}@media (max-width: 719px){.vp-blog-hero{height:350px}}@media (max-width: 419px){.vp-blog-hero{margin:0 0 1rem}}.vp-blog-hero.no-bg{color:var(--text-color)}.vp-blog-hero>:not(.vp-blog-mask){position:relative;z-index:2}.vp-blog-hero .slide-down-button{border-width:0;background:transparent;cursor:pointer;position:absolute;bottom:0;left:calc(50vw - 30px);display:none;width:60px;height:60px;padding:10px}.vp-blog-hero .slide-down-button .icon{width:30px;margin:-15px 0;animation-name:bounce-down;animation-duration:1.5s;animation-timing-function:linear;animation-iteration-count:infinite;animation-direction:alternate}.vp-blog-hero .slide-down-button .icon:first-child{color:#ffffff26}.vp-blog-hero .slide-down-button .icon:nth-child(2){color:#ffffff80}.vp-blog-hero.fullscreen{height:calc(100vh - var(--navbar-height))!important}.vp-blog-hero.fullscreen .vp-blog-mask{background-position-y:top!important}.vp-blog-hero.fullscreen .slide-down-button{display:block}.vp-blog-mask{position:absolute;top:0;right:0;bottom:0;left:0}.vp-blog-mask:after{content:" ";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:block;background:var(--light-grey);opacity:.2}.vp-blog-mask.light{display:block}html[data-theme=dark] .vp-blog-mask.light,.vp-blog-mask.dark{display:none}html[data-theme=dark] .vp-blog-mask.dark{display:block}.vp-blog-hero-title{margin:.5rem auto;font-weight:700;font-size:2rem}@media (min-width: 1440px){.vp-blog-hero-title{font-size:2.25rem}}@media (max-width: 719px){.vp-blog-hero-title{font-size:1.75rem}}.vp-blog-hero-image{display:block;max-width:100%;max-height:15rem;margin:1.5rem auto}@media (max-width: 719px){.vp-blog-hero-image{max-height:12rem}}.vp-blog-hero-image.light{display:block}html[data-theme=dark] .vp-blog-hero-image.light,.vp-blog-hero-image.dark{display:none}html[data-theme=dark] .vp-blog-hero-image.dark{display:block}.vp-blog-hero-image+.vp-blog-hero-title{margin:0 auto}.vp-blog-hero-description{margin:1.2rem auto 0;font-size:1.5rem}@media (max-width: 719px){.vp-blog-hero-description{font-size:1.25rem}}@keyframes bounce-down{0%{transform:translateY(-5px)}to{transform:translateY(5px)}}.vp-blogger-info{padding:.5rem;font-family:var(--font-family-heading);overflow-wrap:break-word}.vp-page .vp-blogger-info{background:var(--bg-color-float);transition:background var(--color-transition)}.vp-sidebar .vp-blogger-info.mobile{display:none}@media (max-width: 719px){.vp-sidebar .vp-blogger-info.mobile{display:block}}.vp-sidebar .vp-blogger-info.mobile+hr{display:none}@media (max-width: 719px){.vp-sidebar .vp-blogger-info.mobile+hr{display:block;margin-top:1rem}}.vp-blogger{padding:.5rem;text-align:center}.vp-blogger-avatar{width:8rem;height:8rem;margin:0 auto}.vp-blogger-avatar.round{border-radius:50%}.vp-blogger-name{margin:1rem auto;font-size:22px}.vp-blogger-description{margin:1rem auto;font-size:14px}.vp-blog-counts{display:flex;width:80%;margin:0 auto 1rem}.vp-blog-count{display:block;width:25%;color:inherit;font-size:13px;text-align:center;cursor:pointer;transition:color var(--color-transition)}.vp-blog-count:hover{color:var(--theme-color)}.vp-blog-count .count{position:relative;margin-bottom:.5rem;font-weight:600;font-size:20px}.vp-category-list{position:relative;z-index:2;padding-inline-start:0;list-style:none;font-size:14px}.vp-category{display:inline-block;vertical-align:middle;overflow:hidden;margin:.3rem .6rem .8rem;padding:.4rem .8rem;border-radius:.25rem;color:var(--dark-grey);box-shadow:0 1px 4px 0 var(--card-shadow);word-break:break-word;cursor:pointer;transition:background var(--color-transition),color var(--color-transition)}@media (max-width: 419px){.vp-category{font-size:.9rem}}.vp-category a{color:inherit}.vp-category .count{display:inline-block;min-width:1rem;height:1.2rem;margin-inline-start:.2em;padding:0 .1rem;border-radius:.6rem;color:var(--white);font-size:.7rem;line-height:1.2rem;text-align:center}.vp-category0{background:#fde5e7;color:#ba111f}html[data-theme=dark] .vp-category0{background:#340509;color:#ec2f3e}.vp-category0:hover{background:#f9bec3}html[data-theme=dark] .vp-category0:hover{background:#53080e}.vp-category0.active{background:#cf1322;color:#fff}html[data-theme=dark] .vp-category0.active{background:#a60f1b;color:var(--bg-color)}.vp-category0.active .count{background:var(--bg-color);color:#cf1322}.vp-category0 .count{background:#cf1322}.vp-category1{background:#ffeee8;color:#f54205}html[data-theme=dark] .vp-category1{background:#441201;color:#fb7649}.vp-category1:hover{background:#fed4c6}html[data-theme=dark] .vp-category1:hover{background:#6d1d02}.vp-category1.active{background:#fa541c;color:#fff}html[data-theme=dark] .vp-category1.active{background:#da3a05;color:var(--bg-color)}.vp-category1.active .count{background:var(--bg-color);color:#fa541c}.vp-category1 .count{background:#fa541c}.vp-category2{background:#fef5e7;color:#e08e0b}html[data-theme=dark] .vp-category2{background:#3e2703;color:#f5b041}.vp-category2:hover{background:#fce6c4}html[data-theme=dark] .vp-category2:hover{background:#633f05}.vp-category2.active{background:#f39c12;color:#fff}html[data-theme=dark] .vp-category2.active{background:#c77e0a;color:var(--bg-color)}.vp-category2.active .count{background:var(--bg-color);color:#f39c12}.vp-category2 .count{background:#f39c12}.vp-category3{background:#eafaf1;color:#29b866}html[data-theme=dark] .vp-category3{background:#0c331c;color:#55d98d}.vp-category3:hover{background:#caf3db}html[data-theme=dark] .vp-category3:hover{background:#12522d}.vp-category3.active{background:#2ecc71;color:#fff}html[data-theme=dark] .vp-category3.active{background:#25a35a;color:var(--bg-color)}.vp-category3.active .count{background:var(--bg-color);color:#2ecc71}.vp-category3 .count{background:#2ecc71}.vp-category4{background:#e6f9ee;color:#219552}html[data-theme=dark] .vp-category4{background:#092917;color:#36d278}.vp-category4:hover{background:#c0f1d5}html[data-theme=dark] .vp-category4:hover{background:#0f4224}.vp-category4.active{background:#25a55b;color:#fff}html[data-theme=dark] .vp-category4.active{background:#1e8449;color:var(--bg-color)}.vp-category4.active .count{background:var(--bg-color);color:#25a55b}.vp-category4 .count{background:#25a55b}.vp-category5{background:#e1fcfc;color:#0e9595}html[data-theme=dark] .vp-category5{background:#042929;color:#16e1e1}.vp-category5:hover{background:#b4f8f8}html[data-theme=dark] .vp-category5:hover{background:#064242}.vp-category5.active{background:#10a5a5;color:#fff}html[data-theme=dark] .vp-category5.active{background:#0d8484;color:var(--bg-color)}.vp-category5.active .count{background:var(--bg-color);color:#10a5a5}.vp-category5 .count{background:#10a5a5}.vp-category6{background:#e4f0fe;color:#0862c3}html[data-theme=dark] .vp-category6{background:#021b36;color:#2589f6}.vp-category6:hover{background:#bbdafc}html[data-theme=dark] .vp-category6:hover{background:#042c57}.vp-category6.active{background:#096dd9;color:#fff}html[data-theme=dark] .vp-category6.active{background:#0757ae;color:var(--bg-color)}.vp-category6.active .count{background:var(--bg-color);color:#096dd9}.vp-category6 .count{background:#096dd9}.vp-category7{background:#f7f1fd;color:#9851e4}html[data-theme=dark] .vp-category7{background:#2a0b4b;color:#bb8ced}.vp-category7:hover{background:#eadbfa}html[data-theme=dark] .vp-category7:hover{background:#431277}.vp-category7.active{background:#aa6fe9;color:#fff}html[data-theme=dark] .vp-category7.active{background:#8733e0;color:var(--bg-color)}.vp-category7.active .count{background:var(--bg-color);color:#aa6fe9}.vp-category7 .count{background:#aa6fe9}.vp-category8{background:#fdeaf5;color:#e81689}html[data-theme=dark] .vp-category8{background:#400626;color:#ef59ab}.vp-category8:hover{background:#facbe5}html[data-theme=dark] .vp-category8:hover{background:#670a3d}.vp-category8.active{background:#eb2f96;color:#fff}html[data-theme=dark] .vp-category8.active{background:#ce147a;color:var(--bg-color)}.vp-category8.active .count{background:var(--bg-color);color:#eb2f96}.vp-category8 .count{background:#eb2f96}html[data-theme=dark] .empty-icon g.people{opacity:.8}html[data-theme=dark] .empty-icon g:not(.people){filter:invert(80%)}.vp-page.vp-blog .vp-blog-home{flex:1;width:0;max-width:780px}.vp-page.vp-blog .theme-hope-content:empty{padding:0}.vp-blog-infos{margin:8px auto;padding:8px 16px}.vp-page .vp-blog-infos{border-radius:6px;background:var(--bg-color-float);box-shadow:0 1px 3px 1px var(--card-shadow);transition:background var(--color-transition),box-shadow var(--color-transition)}.vp-page .vp-blog-infos:hover{box-shadow:0 2px 6px 2px var(--card-shadow)}.vp-blog-infos .timeline-list-wrapper .content{max-height:60vh}.vp-blog-type-switcher{display:flex;justify-content:center;margin-bottom:8px}.vp-blog-type-button{border-width:0;background:transparent;cursor:pointer;width:44px;height:44px;margin:0 8px;padding:4px;color:var(--grey3);transition:color var(--color-transition)}.vp-blog-type-button:focus{outline:none}.vp-blog-type-button .icon-wrapper{width:20px;height:20px;padding:8px;border-radius:50%;background:#7f7f7f26;transition:background var(--color-transition)}html[data-theme=dark] .vp-blog-type-button .icon-wrapper{background:#ffffff26}.vp-blog-type-button .icon-wrapper:hover{cursor:pointer}.vp-blog-type-button .icon-wrapper.active{background:var(--theme-color-light)}html[data-theme=dark] .vp-blog-type-button .icon-wrapper.active{background:var(--theme-color-dark)}.vp-blog-type-button .icon{width:100%;height:100%}.vp-sidebar.hide-icon .vp-blog-type-button .icon{display:block!important}.vp-star-article-wrapper,.vp-category-wrapper,.vp-tag-wrapper{padding:8px 0}.vp-star-article-wrapper .title,.vp-category-wrapper .title,.vp-tag-wrapper .title{cursor:pointer}.vp-star-article-wrapper .title .icon,.vp-category-wrapper .title .icon,.vp-tag-wrapper .title .icon{position:relative;bottom:-.125rem;width:16px;height:16px;margin:0 6px}.vp-star-article-wrapper .title .num,.vp-category-wrapper .title .num,.vp-tag-wrapper .title .num{position:relative;margin:0 2px;font-size:22px;font-family:var(--font-family-heading)}.vp-star-articles{overflow-y:auto;max-height:80vh;margin:8px auto;line-height:1.5}.vp-star-article{padding:12px 8px 4px;border-bottom:1px dashed var(--grey);transition:border-color var(--color-transition),color var(--color-transition)}.vp-star-article a{color:inherit}.vp-star-article:hover{cursor:pointer}.vp-star-article:hover a{color:var(--theme-color)}.vp-category-wrapper .category-list-wrapper,.vp-tag-wrapper .tag-list-wrapper{overflow-y:auto;max-height:80vh;margin:8px auto}.vp-sidebar .vp-blog-info-wrapper .vp-blogger-info{display:none}.vp-page .vp-blog-info-wrapper{position:sticky;top:calc(var(--navbar-height) + .75rem);flex:0 0 300px;box-sizing:border-box;width:300px;height:auto;margin-top:.75rem;margin-bottom:.75rem;margin-inline-start:1rem;transition:all .3s}@media (max-width: 719px){.vp-page .vp-blog-info-wrapper{display:none}}.vp-page .vp-blog-info-wrapper .vp-blogger-info{margin-bottom:16px;padding:8px 0;border-radius:8px;box-shadow:0 1px 3px 1px var(--card-shadow)}.vp-page .vp-blog-info-wrapper .vp-blogger-info:hover{box-shadow:0 2px 6px 2px var(--card-shadow)}.theme-container .vp-page.vp-blog{display:flex;flex-direction:column;justify-content:space-between;box-sizing:border-box;padding-top:var(--navbar-height);padding-bottom:2rem;background:var(--bg-color-back);transition:background var(--color-transition)}@media (min-width: 1440px){.theme-container.has-toc .vp-page.vp-blog{padding-inline-end:0}}.blog-page-wrapper{display:flex;align-items:flex-start;justify-content:center;box-sizing:border-box;width:100%;margin:0 auto;padding:0 2rem}@media (max-width: 959px){.blog-page-wrapper{padding:0 1rem}}@media (max-width: 419px){.blog-page-wrapper{padding:0}}.vp-blog-main{flex:1;width:0;max-width:780px}.vp-pagination{margin:1.25rem 0 .75rem;font-weight:600;font-size:15px;line-height:2}.vp-pagination-list{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-evenly;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vp-pagination-number{display:flex;align-items:stretch;overflow:hidden;height:30px;margin:0 .5rem;border:1px solid var(--border-color);border-radius:.25rem}.vp-pagination-number div{position:relative;padding:0 .5rem;background:var(--bg-color);color:var(--theme-color);cursor:pointer}.vp-pagination-number div:before{content:" ";position:absolute;top:0;bottom:0;inset-inline-start:0;width:1px;background:var(--border-color)}.vp-pagination-number div:first-child:before{background:transparent}.vp-pagination-number div:hover{color:var(--theme-color-light)}.vp-pagination-number div.active{background:var(--theme-color);color:var(--white)}.vp-pagination-number div.active:before{background:var(--theme-color)}.vp-pagination-number div.active+div:before{background:var(--theme-color)}.vp-pagination-number div.prev,.vp-pagination-number div.next{font-size:13px;line-height:30px}.vp-pagination-number div.active,.vp-pagination-number div.ellipsis{cursor:default}.vp-pagination-nav{display:flex;align-items:center;justify-content:center;margin:.5rem}.vp-pagination-nav input{width:3.5rem;margin:6px 5px;border:1px solid var(--border-color);border-radius:.25em;background:var(--bg-color);color:var(--text-color);outline:none;line-height:2;text-align:center}.vp-pagination-button{overflow:hidden;padding:0 .75em;border:1px solid var(--border-color);border-radius:.25em;background:var(--bg-color);color:var(--theme-color);outline:none;font-weight:600;font-size:15px;line-height:2;cursor:pointer}.vp-pagination-button:hover{color:var(--theme-color-light)}.vp-project-panel{position:relative;z-index:2;display:flex;flex-wrap:wrap;align-items:stretch;place-content:stretch flex-start;margin-bottom:12px}.vp-project-panel:empty{margin-bottom:0}.vp-project-card{position:relative;width:calc(33% - 40px);margin:6px 8px;padding:12px;border-radius:8px;background:var(--bg-color-float);transition:background var(--color-transition),transform var(--transform-transition)}@media (max-width: 959px){.vp-project-card{width:calc(50% - 40px)}}@media (min-width: 1440px){.vp-project-card{width:calc(25% - 40px)}}.vp-project-card:hover{cursor:pointer;transform:scale(.98)}.vp-project-card .icon{position:relative;z-index:2;float:right;width:20px;height:20px}html[dir=rtl] .vp-project-card .icon{float:left}.vp-project-card.project0{background:#fde5e7}.vp-project-card.project0:hover{background:#f9bec3}html[data-theme=dark] .vp-project-card.project0{background:#340509}html[data-theme=dark] .vp-project-card.project0:hover{background:#53080e}.vp-project-card.project1{background:#ffeee8}.vp-project-card.project1:hover{background:#fed4c6}html[data-theme=dark] .vp-project-card.project1{background:#441201}html[data-theme=dark] .vp-project-card.project1:hover{background:#6d1d02}.vp-project-card.project2{background:#fef5e7}.vp-project-card.project2:hover{background:#fce6c4}html[data-theme=dark] .vp-project-card.project2{background:#3e2703}html[data-theme=dark] .vp-project-card.project2:hover{background:#633f05}.vp-project-card.project3{background:#eafaf1}.vp-project-card.project3:hover{background:#caf3db}html[data-theme=dark] .vp-project-card.project3{background:#0c331c}html[data-theme=dark] .vp-project-card.project3:hover{background:#12522d}.vp-project-card.project4{background:#e6f9ee}.vp-project-card.project4:hover{background:#c0f1d5}html[data-theme=dark] .vp-project-card.project4{background:#092917}html[data-theme=dark] .vp-project-card.project4:hover{background:#0f4224}.vp-project-card.project5{background:#e1fcfc}.vp-project-card.project5:hover{background:#b4f8f8}html[data-theme=dark] .vp-project-card.project5{background:#042929}html[data-theme=dark] .vp-project-card.project5:hover{background:#064242}.vp-project-card.project6{background:#e4f0fe}.vp-project-card.project6:hover{background:#bbdafc}html[data-theme=dark] .vp-project-card.project6{background:#021b36}html[data-theme=dark] .vp-project-card.project6:hover{background:#042c57}.vp-project-card.project7{background:#f7f1fd}.vp-project-card.project7:hover{background:#eadbfa}html[data-theme=dark] .vp-project-card.project7{background:#2a0b4b}html[data-theme=dark] .vp-project-card.project7:hover{background:#431277}.vp-project-card.project8{background:#fdeaf5}.vp-project-card.project8:hover{background:#facbe5}html[data-theme=dark] .vp-project-card.project8{background:#400626}html[data-theme=dark] .vp-project-card.project8:hover{background:#670a3d}.vp-project-name{position:relative;z-index:2;color:var(--grey3);font-weight:500;font-size:16px;transition:color var(--color-transition)}.vp-project-desc{position:relative;z-index:2;margin:6px 0;color:var(--dark-grey);font-size:13px}.vp-project-image{position:relative;z-index:2;float:right;width:40px;height:40px}html[dir=rtl] .vp-project-image{float:left}.vp-social-medias{display:flex;flex-wrap:wrap;justify-content:center;margin:8px auto}.vp-social-media{width:26px;height:26px;margin:4px;transition:transform .18s ease-out .18s;transform:scale(1)}.vp-social-media:hover{cursor:pointer;transform:scale(1.2)}.vp-social-media:after{--balloon-font-size: 8px;padding:.3em .6em}.vp-social-media .icon{width:100%;height:100%}.tag-list-wrapper{position:relative;z-index:2;display:flex;flex-wrap:wrap;justify-content:flex-start;padding-inline-start:0;list-style:none}.tag-list-wrapper a{color:inherit}.tag-list-wrapper .tag{position:relative;display:inline-block;vertical-align:middle;overflow:hidden;min-width:24px;margin:4px 6px;padding:3px 8px;border-radius:8px;color:var(--white);box-shadow:0 1px 6px 0 var(--box-shadow);font-size:12px;text-align:center;word-break:break-word;cursor:pointer;transition:background var(--color-transition),box-shadow var(--color-transition),transform var(--color-transition)}.tag-list-wrapper .tag:hover{cursor:pointer}.tag-list-wrapper .tag.active{transform:scale(1.1)}.tag-list-wrapper .tag-num{margin-inline-start:.5em}.tag-list-wrapper .tag0{background:#e91526}.tag-list-wrapper .tag0:hover,.tag-list-wrapper .tag0.active,html[data-theme=dark] .tag-list-wrapper .tag0{background:#c51220}html[data-theme=dark] .tag-list-wrapper .tag0:hover,html[data-theme=dark] .tag-list-wrapper .tag0.active{background:#e91526}.tag-list-wrapper .tag1{background:#fb6533}.tag-list-wrapper .tag1:hover,.tag-list-wrapper .tag1.active,html[data-theme=dark] .tag-list-wrapper .tag1{background:#fa4a0e}html[data-theme=dark] .tag-list-wrapper .tag1:hover,html[data-theme=dark] .tag-list-wrapper .tag1.active{background:#fb6533}.tag-list-wrapper .tag2{background:#f4a62a}.tag-list-wrapper .tag2:hover,.tag-list-wrapper .tag2.active,html[data-theme=dark] .tag-list-wrapper .tag2{background:#ec950c}html[data-theme=dark] .tag-list-wrapper .tag2:hover,html[data-theme=dark] .tag-list-wrapper .tag2.active{background:#f4a62a}.tag-list-wrapper .tag3{background:#40d47f}.tag-list-wrapper .tag3:hover,.tag-list-wrapper .tag3.active,html[data-theme=dark] .tag-list-wrapper .tag3{background:#2cc26b}html[data-theme=dark] .tag-list-wrapper .tag3:hover,html[data-theme=dark] .tag-list-wrapper .tag3.active{background:#40d47f}.tag-list-wrapper .tag4{background:#2bbe69}.tag-list-wrapper .tag4:hover,.tag-list-wrapper .tag4.active,html[data-theme=dark] .tag-list-wrapper .tag4{background:#239d56}html[data-theme=dark] .tag-list-wrapper .tag4:hover,html[data-theme=dark] .tag-list-wrapper .tag4.active{background:#2bbe69}.tag-list-wrapper .tag5{background:#13c3c3}.tag-list-wrapper .tag5:hover,.tag-list-wrapper .tag5.active,html[data-theme=dark] .tag-list-wrapper .tag5{background:#0f9d9d}html[data-theme=dark] .tag-list-wrapper .tag5:hover,html[data-theme=dark] .tag-list-wrapper .tag5.active{background:#13c3c3}.tag-list-wrapper .tag6{background:#0a7bf4}.tag-list-wrapper .tag6:hover,.tag-list-wrapper .tag6.active,html[data-theme=dark] .tag-list-wrapper .tag6{background:#0968ce}html[data-theme=dark] .tag-list-wrapper .tag6:hover,html[data-theme=dark] .tag-list-wrapper .tag6.active{background:#0a7bf4}.tag-list-wrapper .tag7{background:#b37deb}.tag-list-wrapper .tag7:hover,.tag-list-wrapper .tag7.active,html[data-theme=dark] .tag-list-wrapper .tag7{background:#a160e7}html[data-theme=dark] .tag-list-wrapper .tag7:hover,html[data-theme=dark] .tag-list-wrapper .tag7.active{background:#b37deb}.tag-list-wrapper .tag8{background:#ed44a1}.tag-list-wrapper .tag8:hover,.tag-list-wrapper .tag8.active,html[data-theme=dark] .tag-list-wrapper .tag8{background:#ea2290}html[data-theme=dark] .tag-list-wrapper .tag8:hover,html[data-theme=dark] .tag-list-wrapper .tag8.active{background:#ed44a1}.timeline-wrapper{--dot-color: #fff;--dot-bar-color: #eaecef;--dot-border-color: #ddd;max-width:740px;margin:0 auto;padding:40px 0}@media (max-width: 719px){.timeline-wrapper{margin:0 1.2rem}}html[data-theme=dark] .timeline-wrapper{--dot-color: #444;--dot-bar-color: #333;--dot-border-color: #555}.timeline-wrapper #toc{inset-inline:unset 0;min-width:0}.timeline-wrapper .toc-wrapper{position:relative;z-index:10}.timeline-wrapper .timeline-content{position:relative;box-sizing:border-box;padding-inline-start:76px;list-style:none}.timeline-wrapper .timeline-content:after{content:" ";position:absolute;top:14px;inset-inline-start:64px;z-index:-1;width:4px;height:calc(100% - 38px);margin-inline-end:-2px;background:var(--dot-bar-color);transition:background var(--color-transition)}.timeline-wrapper .motto{position:relative;color:var(--text-color);font-size:18px;transition:color var(--color-transition)}@media (min-width: 1280px){.timeline-wrapper .motto{font-size:20px}}.timeline-wrapper .motto:before{content:" ";position:absolute;top:50%;z-index:2;margin-top:-6px;margin-inline-start:-6px;border:2px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--color-transition),border-color var(--color-transition);inset-inline-start:-10px;width:8px;height:8px}.timeline-wrapper .timeline-year-title{margin-top:calc(3rem - var(--navbar-height));margin-bottom:.5rem;padding-top:var(--navbar-height);color:var(--text-color);font-weight:700;font-size:26px;font-family:var(--font-family-heading);transition:color var(--color-transition)}.timeline-wrapper .timeline-year-title span{position:relative}.timeline-wrapper .timeline-year-title span:before{content:" ";position:absolute;top:50%;z-index:2;margin-top:-6px;margin-inline-start:-6px;border:2px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--color-transition),border-color var(--color-transition);inset-inline-start:-10px;width:8px;height:8px}.timeline-wrapper .timeline-year-wrapper{padding-inline-start:0!important}.timeline-wrapper .timeline-date{position:absolute;inset-inline-end:calc(100% + 24px);width:50px;font-size:14px;line-height:30px;text-align:end}.timeline-wrapper .timeline-date:before{content:" ";position:absolute;top:50%;z-index:2;margin-top:-6px;margin-inline-start:-6px;border:2px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--color-transition),border-color var(--color-transition);inset-inline-end:-19px;width:6px;height:6px}.timeline-wrapper .timeline-title{position:relative;display:block;color:inherit;font-size:16px;line-height:30px;transition:color var(--color-transition),font-size var(--transform-transition)}.timeline-wrapper .timeline-item{position:relative;z-index:3;display:flex;padding:30px 0 10px;border-bottom:1px dashed var(--border-color);list-style:none;transition:border-color var(--color-transition)}.timeline-wrapper .timeline-item:hover{cursor:pointer}.timeline-wrapper .timeline-item:hover .timeline-date{font-size:16px;transition:border-color var(--color-transition),color var(--color-transition),font-size var(--transform-transition)}.timeline-wrapper .timeline-item:hover .timeline-date:before{border-color:var(--theme-color);background:var(--bg-color-secondary)}.timeline-wrapper .timeline-item:hover .timeline-title{color:var(--theme-color);font-size:18px}.timeline-list-wrapper{--dot-color: #fff;--dot-bar-color: #eaecef;--dot-border-color: #ddd;padding:8px 0}html[data-theme=dark] .timeline-list-wrapper{--dot-color: #444;--dot-bar-color: #333;--dot-border-color: #555}.timeline-list-wrapper .timeline-list-title{cursor:pointer}.timeline-list-wrapper .timeline-list-title .icon{position:relative;bottom:-.125rem;width:16px;height:16px;margin:0 6px}.timeline-list-wrapper .timeline-list-title .num{position:relative;margin:0 2px;font-size:22px}.timeline-list-wrapper .timeline-content{overflow-y:auto;max-height:80vh}.timeline-list-wrapper .timeline-content::-webkit-scrollbar-track-piece{background:transparent}.timeline-list-wrapper .timeline-list{position:relative;box-sizing:border-box;margin:0 8px;list-style:none}.timeline-list-wrapper .timeline-list:after{content:" ";position:absolute;top:14px;inset-inline-start:0;z-index:-1;width:4px;height:calc(100% - 14px);margin-inline-start:-2px;background:var(--dot-bar-color);transition:background var(--color-transition)}.timeline-list-wrapper .timeline-year{position:relative;margin:20px 0 0;color:var(--text-color);font-weight:700;font-size:20px}.timeline-list-wrapper .timeline-year:before{content:" ";position:absolute;z-index:2;border:1px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--color-transition),border-color var(--color-transition);top:50%;inset-inline-start:-20px;width:8px;height:8px;margin-top:-4px;margin-inline-start:-4px}.timeline-list-wrapper .timeline-year-wrapper{padding-inline-start:0!important}.timeline-list-wrapper .timeline-date{display:inline-block;vertical-align:bottom;width:36px;font-size:12px;line-height:32px;transition:color var(--color-transition)}.timeline-list-wrapper .timeline-date:before{content:" ";position:absolute;z-index:2;border:1px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--color-transition),border-color var(--color-transition);top:24px;inset-inline-start:-19px;width:6px;height:6px;margin-inline-start:-4px}.timeline-list-wrapper .timeline-title{color:inherit;font-size:14px;line-height:32px;cursor:pointer;transition:color var(--color-transition)}.timeline-list-wrapper .timeline-item{position:relative;display:flex;padding:12px 0 4px;border-bottom:1px dashed var(--border-color);list-style:none;transition:border-color var(--color-transition)}.timeline-list-wrapper .timeline-item:hover .timeline-date{color:var(--theme-color)}.timeline-list-wrapper .timeline-item:hover .timeline-date:before{border-color:var(--dot-color);background:var(--theme-color)}.timeline-list-wrapper .timeline-item:hover .timeline-title{color:var(--theme-color)}:root{--navbar-bg-color: var(--bg-color-float-blur);--sidebar-bg-color: var(--bg-color-blur)}html[data-theme=dark]{--navbar-bg-color: var(--bg-color-blur);--sidebar-bg-color: var(--bg-color-blur)}#app{--code-hl-bg-color: var(--code-highlight-line-color);--code-ln-color: var(--code-line-color);--code-ln-wrapper-width: var(--line-numbers-width);--code-tabs-nav-text-color: var(--code-color);--code-tabs-nav-bg-color: var(--code-border-color);--code-tabs-nav-hover-color: var(--code-highlight-line-color);--sidebar-space: var(--sidebar-width)}@media (max-width: 959px){#app{--navbar-height: var(--navbar-mobile-height);--navbar-vertical-padding: var(--navbar-mobile-vertical-padding);--navbar-horizontal-padding: var(--navbar-mobile-horizontal-padding);--sidebar-width: var(--sidebar-mobile-width)}}@media (min-width: 1440px){#app{--sidebar-space: clamp( var(--sidebar-width), max(0px, calc((100vw - var(--content-width)) / 2 - 2rem)) , 100vw )}}.DocSearch-Button,.DocSearch{--docsearch-primary-color: var(--vp-tc);--docsearch-text-color: var(--vp-c);--docsearch-highlight-color: var(--vp-tc);--docsearch-muted-color: var(--light-grey);--docsearch-container-background: rgb(9 10 17 / 80%);--docsearch-modal-background: var(--bg-color-float);--docsearch-searchbox-background: var(--bg-color-secondary);--docsearch-searchbox-focus-background: var(--vp-bg);--docsearch-searchbox-shadow: inset 0 0 0 2px var(--vp-tc);--docsearch-hit-color: var(--vp-cl);--docsearch-hit-active-color: var(--vp-bg);--docsearch-hit-background: var(--vp-bg);--docsearch-hit-shadow: 0 1px 3px 0 var(--border-color);--docsearch-footer-background: var(--vp-bg)}html[data-theme=dark] .DocSearch-Button,html[data-theme=dark] .DocSearch{--docsearch-logo-color: var(--vp-c);--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgb(3 4 9 / 30%);--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);--docsearch-footer-shadow: inset 0 1px 0 0 rgb(73 76 106 / 50%), 0 -4px 8px 0 rgb(0 0 0 / 20%)}#nprogress{--nprogress-color: var(--vp-tc)}.search-box{--search-bg-color: var(--vp-bg);--search-accent-color: var(--vp-tc);--search-text-color: var(--vp-c);--search-border-color: var(--border-color);--search-item-text-color: var(--vp-clt);--search-item-focus-bg-color: var(--bg-color-secondary)}.external-link-icon{--external-link-icon-color: var(--light-grey)}html,body{margin:0;padding:0;background:#fff}html{font-size:16px;font-display:optional;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-tap-highlight-color:transparent}@media print{html{font-size:12pt}}body{min-height:100vh;color:#2c3e50}a{color:#3eaf7c;font-weight:500;text-decoration:none;overflow-wrap:break-word}kbd{display:inline-block;min-width:1em;margin-inline:.125rem;padding:.25em;border:1px solid #eee;border-radius:.25em;box-shadow:1px 1px 4px #00000026;line-height:1;letter-spacing:-.1em;text-align:center}code{margin:0;padding:.2rem .4rem;border-radius:5px;background:#7f7f7f1f;font-size:.85em;overflow-wrap:break-word}table code{padding:.1rem .4rem}p a code{color:#3eaf7c;font-weight:400}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:500;line-height:1.25;overflow-wrap:break-word}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2rem}h2{padding-bottom:.3rem;border-bottom:1px solid #eaecef;font-size:1.65rem}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}a.header-anchor{float:left;margin-top:.125em;margin-inline-start:-.87em;padding-inline-end:.23em;font-size:.85em;opacity:0;transition:opacity .2s}@media print{a.header-anchor{display:none!important}}a.header-anchor:hover{text-decoration:none}a.header-anchor:focus-visible{opacity:1}p,ul,ol{line-height:1.6;overflow-wrap:break-word}@media print{p,ul,ol{line-height:1.5}}ul,ol{padding-inline-start:1.2em}blockquote{margin:1rem 0;padding:.25rem 0 .25rem 1rem;border-inline-start:.2rem solid #ddd;color:#666;font-size:1rem;overflow-wrap:break-word}blockquote>p{margin:0}hr{border:0;border-top:1px solid #eaecef}table{display:block;overflow-x:auto;margin:1rem 0;border-collapse:collapse}tr:nth-child(2n){background:#f6f8fa}th,td{padding:.6em 1em;border:1px solid #dfe2e5}pre{direction:ltr}@page{margin:2cm;font-size:12pt;size:a4}@media print{*,:after,:before{box-shadow:none!important;text-shadow:none!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}a{color:inherit;font-weight:inherit!important;font-size:inherit!important;text-decoration:underline}a[href^="http://"]:after,a[href^="https://"]:after{content:" (" attr(href) ") "}abbr[title]:after{content:" (" attr(title) ")"}pre{border:1px solid #eee;white-space:pre-wrap!important}pre>code{white-space:pre-wrap!important}blockquote{border-inline-start:.2rem solid #ddd;color:inherit}blockquote,pre{orphans:5;widows:5}img,tr,canvas{page-break-inside:avoid}}@font-face{font-weight:400;font-style:normal;font-family:Crimson;src:url(data:font/truetype;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTYr5mwEAAAyMAAAAHEdERUYAKQATAAAMbAAAAB5PUy8yVsJ0MgAAAVgAAABgY21hcBiKDzgAAAHcAAABWGdhc3D//wADAAAMZAAAAAhnbHlmr+DBdQAAA1AAAAdsaGVhZBZwt+8AAADcAAAANmhoZWEFawEuAAABFAAAACRobXR4BksA9gAAAbgAAAAibG9jYQlsC24AAAM0AAAAHG1heHAAEQBZAAABOAAAACBuYW1lLaFDVAAACrwAAAFrcG9zdAC1AHoAAAwoAAAAPAABAAAAAQAAqBd2H18PPPUACwQAAAAAANqqufwAAAAA2qq5/AAb/9wB4QMeAAAACAACAAAAAAAAAAEAAAMs/ywAXAH9AAAAAAHhAAEAAAAAAAAAAAAAAAAAAAAEAAEAAAANAFkAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAH1AZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAIABgMAAAAAAAAAAAABEAAAAAAAAAAAAAAAUGZFZADAADAAOQMs/ywAXAMsANQAAAABAAAAAAMYAAAAAAAgAAEBpwAfAAAAAAFVAAAB/QAfAH0ALQA+ABsAPgAyACgAPgAxAAAAAAADAAAAAwAAABwAAQAAAAAAUgADAAEAAAAcAAQANgAAAAQABAABAAAAOf//AAAAL///AAAAAQAEAAAAAAADAAQABQAGAAcACAAJAAoACwAMAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAwQFBgcICQoLDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACYAJgAmAGIAwAEeAZIBzgJAApYC2gNiA7YAAQAf/9wBhwMeABIAAAEGBwYHATAXFjM2NzY3ASYnJjcBgxwLCgH+zgMECxIKCgIBLgEDAwMDHhQFBgP85wMEAQgJBgMOAwMDEwAAAAIAH//9Ad0CkAAQACEAABMWFxYXNjc2NzQnJicGBwYHNyY3NjcWFxYXFAcGByYnJjcfATo6amo7OQE5OmxrOjkBXQIlJEE5IyIBIyJEOSQjAgFOkV5eBAReXoqJXl4EBF5eggJ0UlEDA09Qe3xVVgMDU1OEAAAAAAEAff/9AYACkQA+AAA3FAcGBwYHBiMGFQYXNjc2MzIXFhc2JzQnIicmJyY1JjURNjc2MSYnJicjBgcGBwYVFBUUFxYXNjc2NzIXFhXkAQEEBRgYDAMBBB4ZGhweGxofBAEDDBgZBQQBAQMEAQIDBAIFNTZCAgMDBA0XFw0LBQV3GBMVDAgEBAUKCgUCAQICAQIFCgoFBAQIDBUTGAGnLxkbBAYFAQIZGh4BAgECBQUEAwUHBwEICRYAAAAAAQAtAAAB0QKRADoAADcGFxYXITY3NjcmJyYjIgcGBwYHBisBNjc2NzY3NjUmJyYnBgcGBxQXFhc2NzY3FhcWFxYHBgcGBwYHLgEEAwMBYwURERADBwYFBAMDAg8VEx/LJkBAOhsQDwIxMkxSMjIHCAYGCSYmPTIfHwEBCgoeLkJBQg8EBQQCETAwKQICAgEBBCgUEylJSUYhJicsRDIzAgY1NRoEBQYBEyEhAwEjIjYlJCQtQlBQSAAAAAABAD7/+wG+ApEASgAANwYXFhcWFxYzNjc2NyYnJic2NzY3JicmIwYHBgcUFxYXNjc2NxYXFhcGBwYHBgcUFRQXNjc2NxYXFhcGBwYnIicmJyYnJiciBwYXPwEIBwUaHB0VZU5NBAMvLi8eIB4DAywsKzwrKxgEAwUIHR4wLRscAQMvLz8BAQYKEhEQNSYmAgImJSsWExQPCw0NFREMDQE7DgsLBQwFBgE8PWpMKSoGECQkMkAiIQIdHyUHBwcBCRscAwEbGSpCIyUOAgMCAwwIAwUEAQEoKD9XJSQBBQYODg8PAQ0NFQAAAgAb//oB4QKTACIAJQAANxQXFhchFRQXFjMyNzYjNTM2NzY1NCcmJyMRNCcmIwYHBgcBExEbAgMFASEJCRIdCAkBRgIBAQUEBTwFAwgHCQkG/vjmxgUGBgOwBQIBAwKzAgQDCBAMDQEBlAYGBgEICQf+cwEs/tQAAQA+//sBvgKTAEoAADcGFxYXFhcWMzY3NjcmJyYnIgcGBzY3NjczMjc2NzY3NjU0JyYnBgcGByMGBwYHFBcWMzY3NjMWFxYHBgcGJyInJicmJyYnIgcGFz8BCAcFGhwdFWVOTQQBMjJbFx8gFwoJCQlWKB0dFQ4JCAQDBQMdHSKXCREQEgMCBA4bGhNYJyUBAiYlKxYTFA8LDQ0VEQwNATsOCwsFDAUGATw9akU2NwMFBggrMC8uAgICExcZBgQCAgMBAwQBMVNUWAUFBAYFBAMxMTNZIyQBBQYODg8PAQ0NFQAAAgAy//oBzQKXACAAMwAANxQXFhc2NzY3NicmJyIHBgc2NzY3NCcmJwYHBgcGBwYXNyY3Njc2FxYXFgcGBwYHJicmNzM1NV5aOTsCAioqahoiIRsnWFhFAwIHQ0tMOTAZGQFbBAQaGxkXRB8fAQEfIDE9Hh4E511FRwQDPT1ZPEJBBQwLF4Y9PRMGCwwBEiwsPDZFRkkTHyAbCAcBAjAwREYsLQEFREVQAAAAAAEAKP/7AdUCiwApAAATFhcWMzI3Njc2NzYzIQYHBgcWFxYzMjcBNjc2NzQnJiMiBwYjIQYHBgcoAwYHAwYDAwELEBEdAQUJYWJXAQ8PDgcDAQ4LCQgBAQEEBhUVFv7JBgsNDAH6DQMCAQEFKRITFMjHjQcFBgMCPxYSEwoEAgMBAhkrKiAAAAADAD7/9wG/ApIAKABBAFgAADcGFxYXNjc2NyYnJicmJzQ3Njc2NyYnJiMGBwYHFhcWFxYVFAcGBwYHNyY3Njc2MzIzMhcyFxYXFhcGBwYHIicmNxMmNzY3FhcWFRQHBgcGByIjIicmJyY3PwE1M1ZQODgDAykpMQIBAyYlJQMCMC9HRjExAgIiIiMCAiMvLwNTBBQTKgEBAQECAQIBEjU1CAEdHjMrISICGAMYGSYvGxoTEx8CAQIBBAMfJCQBoU8tLQECMjFPOC4uGwIBAgEWJiU7SCYoAjEwQzopKhMBAgECEykpQAQsIiEbAQEBBywsQjUeHQEiI0QBZSMhIAECJiYvKh8gFAEBAhAfIEYAAAIAMf/6AcsClwAgADMAABMGFxYXMjc2NwYHBgcUFxYXNjc2NzY3NjUmJyYnBgcGBzcmNzY3FhcWFRQHBgcGJyYnJjc0AyopahoiIRsoV1hFAwIHQ0tMODEZGQE2NF5ZOjoBWgMfHzE9Hh4EGhoaF0QeHwUBy0dBQgUMCxeFPj0SBwsLAREsLD01RkVPV0dFBQQ8PU8UPCwtAQVFRUklIRsHCAECMDBPAAAADACWAAEAAAAAAAEABwAQAAEAAAAAAAIABwAoAAEAAAAAAAMABwBAAAEAAAAAAAQABwBYAAEAAAAAAAUAHgCeAAEAAAAAAAYABwDNAAMAAQQJAAEADgAAAAMAAQQJAAIADgAYAAMAAQQJAAMADgAwAAMAAQQJAAQADgBIAAMAAQQJAAUAPABgAAMAAQQJAAYADgC9AEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAFYAZQByAHMAaQBvAG4AIAAxAC4AMAA7ACAARgBvAG4AdABFAGQAaQB0AG8AcgAgACgAdgAxAC4AMAApAABWZXJzaW9uIDEuMDsgRm9udEVkaXRvciAodjEuMCkAAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAAACAAAAAAAAADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAABAAIAEwAUABUAFgAXABgAGQAaABsAHAAAAAH//wACAAEAAAAMAAAAFgAAAAIAAQADAAwAAQAEAAAAAgAAAAAAAAABAAAAANWkJwgAAAAA2qq5/AAAAADaqrn8) format("truetype")}html,body{background:var(--bg-color);transition:background var(--color-transition)}:root{color-scheme:light}html[data-theme=dark]{color-scheme:dark}body{color:var(--text-color);font-family:var(--font-family)}@media (min-width: 1440px){body{font-size:17px}}a{color:var(--theme-color)}kbd{border-color:var(--border-color-dark);background:var(--bg-color-secondary);font-family:var(--font-family-mono)}code{font-family:var(--font-family-mono);transition:background var(--color-transition),color var(--color-transition)}html[data-theme=dark] code{background:#333}p a code{color:var(--theme-color)}blockquote{border-color:#eee;color:#666;transition:border-color var(--color-transition),color var(--color-transition)}html[data-theme=dark] blockquote{border-color:#333}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-heading)}@media (max-width: 419px){h1{font-size:1.9rem}}h2{border-color:var(--border-color);transition:border-bottom-color var(--color-transition)}hr{border-color:var(--border-color);transition:border-top-color var(--color-transition)}tr:nth-child(2n){background:var(--bg-color-secondary)}th,td{border-color:var(--border-color-dark)}@media print{@page{--text-color: #000 !important;--bg-color: #fff !important}div[class*=language-]{position:relative!important}}.theme-hope-content:not(.custom)>*:first-child{margin-top:0}.vp-breadcrumb{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem;position:relative;z-index:2;padding-top:1rem;font-size:15px}@media (max-width: 959px){.vp-breadcrumb{padding-inline:1.5rem}}@media print{.vp-breadcrumb{max-width:unset}}@media (max-width: 959px){.vp-breadcrumb{font-size:14px}}@media (max-width: 419px){.vp-breadcrumb{padding-top:.5rem;font-size:12.8px}}@media print{.vp-breadcrumb{display:none}}.vp-breadcrumb .icon{margin-inline-end:.25em;font-size:1em}.vp-breadcrumb img.icon{vertical-align:-.125em;height:1em}.vp-breadcrumb a{display:inline-block;padding:0 .5em}.vp-breadcrumb a:before{position:relative;bottom:.125rem;margin-inline-end:.25em}.vp-breadcrumb a:hover{color:var(--theme-color)}.vp-breadcrumb ol{margin:0;padding-inline-start:0;list-style:none}.vp-breadcrumb li{display:inline-block;line-height:1.5}.vp-breadcrumb li:first-child a{padding-inline-start:0}.vp-breadcrumb li:last-child a{padding-inline-end:0}.vp-breadcrumb li.is-active a{color:var(--light-grey);cursor:default;pointer-events:none}.vp-breadcrumb li+li:before{content:"/";color:var(--light-grey)}.toggle-sidebar-wrapper{position:fixed;top:var(--navbar-height);bottom:0;inset-inline-start:var(--sidebar-space);z-index:100;display:flex;align-items:center;justify-content:center;font-size:2rem;transition:inset-inline-start var(--transform-transition)}@media (max-width: 719px){.toggle-sidebar-wrapper{display:none}}@media (min-width: 1440px){.toggle-sidebar-wrapper{display:none}}.toggle-sidebar-wrapper:hover{background:#7f7f7f0d;cursor:pointer}.toggle-sidebar-wrapper .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}html[data-theme=dark] .toggle-sidebar-wrapper .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.toggle-sidebar-wrapper .arrow.down{transform:rotate(180deg)}html[dir=rtl] .toggle-sidebar-wrapper .arrow.down{transform:rotate(-180deg)}.toggle-sidebar-wrapper .arrow.end{transform:rotate(90deg)}html[dir=rtl] .toggle-sidebar-wrapper .arrow.end,.toggle-sidebar-wrapper .arrow.start{transform:rotate(-90deg)}html[dir=rtl] .toggle-sidebar-wrapper .arrow.start{transform:rotate(90deg)}.theme-container{display:flex;flex-direction:column;justify-content:space-between;min-height:100vh}.theme-container .vp-page{padding-top:var(--navbar-height);padding-inline-start:calc(var(--sidebar-space) + 2rem)}@media (max-width: 719px){.theme-container .vp-page{padding-inline:0}}@media (min-width: 1440px){.theme-container .vp-page{padding-inline-end:calc(100vw - var(--content-width) - var(--sidebar-space) - 6rem)}}.theme-container .vp-sidebar{top:var(--navbar-height)}.theme-container.no-navbar .vp-page{padding-top:0}.theme-container.no-navbar .vp-sidebar{top:0}@media (max-width: 719px){.theme-container.no-navbar .vp-sidebar{top:0}}@media (max-width: 719px){.theme-container.hide-navbar .vp-sidebar{top:0}}.theme-container.sidebar-collapsed .vp-page{padding-inline-start:0}.theme-container.sidebar-collapsed .vp-sidebar{box-shadow:none;transform:translate(-100%)}html[dir=rtl] .theme-container.sidebar-collapsed .vp-sidebar{transform:translate(100%)}.theme-container.sidebar-collapsed .toggle-sidebar-wrapper{inset-inline-start:0}.theme-container.no-sidebar .vp-page{padding-inline:0}@media (min-width: 1440px){.theme-container.no-sidebar.has-toc .vp-page{padding-inline-end:16rem}}.theme-container.no-sidebar .vp-toggle-sidebar-button,.theme-container.no-sidebar .toggle-sidebar-wrapper,.theme-container.no-sidebar .vp-sidebar{display:none}.theme-container.sidebar-open .vp-sidebar{box-shadow:2px 0 8px var(--card-shadow);transform:translate(0)}.fade-slide-y-enter-active{transition:all .3s ease!important}.fade-slide-y-leave-active{transition:all .3s cubic-bezier(1,.5,.8,1)!important}.fade-slide-y-enter-from,.fade-slide-y-leave-to{opacity:0;transform:translateY(10px)}.vp-feature-wrapper{position:relative}.vp-feature-bg{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;background-attachment:fixed;background-position:50%;background-size:cover}.vp-feature-bg.light{display:inline-block}.vp-feature-bg.dark,html[data-theme=dark] .vp-feature-bg.light{display:none}html[data-theme=dark] .vp-feature-bg.dark{display:inline-block}.vp-feature{position:relative;z-index:1;margin:0 auto;padding:1.5rem 1rem;color:var(--text-color-lighter);text-align:center}.vp-feature-bg+.vp-feature{color:#222}html[data-theme=dark] .vp-feature-bg+.vp-feature{color:#eee}.vp-feature-bg+.vp-feature .icon{color:inherit}.vp-feature-image{height:10rem;margin:0 auto}@media (max-width: 959px){.vp-feature-image{height:8rem}}.vp-feature-image.light{display:inline-block}.vp-feature-image.dark,html[data-theme=dark] .vp-feature-image.light{display:none}html[data-theme=dark] .vp-feature-image.dark{display:inline-block}.vp-feature-header{margin-bottom:1.5rem;border-bottom:none;font-size:3rem;font-family:var(--font-family);text-align:center}@media (max-width: 959px){.vp-feature-header{font-size:2.5rem}}@media (max-width: 719px){.vp-feature-header{font-size:2.25rem}}@media (max-width: 419px){.vp-feature-header{font-size:2rem}}.vp-feature-description{font-size:1.125rem}.vp-features{z-index:1;display:flex;flex-wrap:wrap;align-items:stretch;place-content:stretch center;margin:1rem 0;text-align:start}@media print{.vp-features{display:block}}.vp-features:first-child{border-top:1px solid var(--border-color);transition:border-color var(--color-transition)}.vp-feature-item{position:relative;display:block;flex-basis:calc(33% - 3rem);margin:.5rem;padding:1rem;border-radius:.5rem;color:inherit;transition:background var(--color-transition),box-shadow var(--color-transition),transform var(--transform-transition)}@media (min-width: 1440px){.vp-feature-item{flex-basis:calc(25% - 3rem)}}@media (max-width: 959px){.vp-feature-item{flex-basis:calc(50% - 3rem)}}@media (max-width: 719px){.vp-feature-item{flex-basis:100%;font-size:.95rem}}@media (max-width: 419px){.vp-feature-item{margin:.5rem 0;font-size:.9rem}}.vp-feature-item.link{cursor:pointer}@media print{.vp-feature-item.link{text-decoration:none}}.vp-feature-item .icon{display:inline-block;height:1.1em;margin-inline-end:.5rem;color:var(--theme-color);font-weight:400;font-size:1.1em}.vp-feature-item:hover{background-color:var(--bg-color-secondary);box-shadow:0 2px 12px 0 var(--card-shadow);transform:translate(-2px,-2px);transform:scale(1.05)}.vp-feature-bg+.vp-feature .vp-feature-item:hover{background-color:transparent}.vp-feature-item:only-child{flex-basis:100%}.vp-feature-item:first-child:nth-last-child(2),.vp-feature-item:nth-child(2):last-child{flex-basis:calc(50% - 3rem)}@media (max-width: 719px){.vp-feature-item:first-child:nth-last-child(2),.vp-feature-item:nth-child(2):last-child{flex-basis:100%}}.vp-feature-title{margin:.25rem 0 .5rem;font-weight:700;font-size:1.3rem;font-family:var(--font-family)}@media (max-width: 419px){.vp-feature-title{font-size:1.2rem}}.vp-feature-details{margin:0;line-height:1.4}.vp-footer-wrapper{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-evenly;padding-block:.75rem;padding-inline:calc(var(--sidebar-space) + 2rem) 2rem;border-top:1px solid var(--border-color);background:var(--bg-color);color:var(--dark-grey);text-align:center;transition:border-top-color var(--color-transition),background var(--color-transition),padding var(--transform-transition)}@media (max-width: 719px){.vp-footer-wrapper{padding-inline-start:2rem}}@media (min-width: 1440px){.vp-footer-wrapper{z-index:50;padding-inline-start:2rem}}@media print{.vp-footer-wrapper{margin:0!important;padding:0!important}}@media (max-width: 419px){.vp-footer-wrapper{display:block}}.no-sidebar .vp-footer-wrapper,.sidebar-collapsed .vp-footer-wrapper{padding-inline-start:2rem}.vp-footer{margin:.5rem 1rem;font-size:14px}@media print{.vp-footer{display:none}}.vp-copyright{margin:6px 0;font-size:13px}.vp-page:not(.not-found)+.vp-footer-wrapper{margin-top:-2rem}.vp-hero-info-wrapper{position:relative;display:flex;align-items:center;justify-content:center;margin-inline:auto}.vp-hero-info-wrapper.fullscreen{height:calc(100vh - var(--navbar-height))!important}.vp-hero-info{z-index:1;width:100%;padding-inline:2.5rem}@media (max-width: 959px){.vp-hero-info{padding-inline:1.5rem}}@media (min-width: 959px){.vp-hero-info{display:flex;align-items:center;justify-content:space-evenly}}.vp-hero-mask{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;background-position:50%;background-size:cover}.vp-hero-mask:after{content:" ";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:block}.vp-hero-mask.light{display:block}html[data-theme=dark] .vp-hero-mask.light,.vp-hero-mask.dark{display:none}html[data-theme=dark] .vp-hero-mask.dark{display:block}.vp-hero-infos{z-index:1;margin:0 .5rem}.vp-hero-image{display:block;max-width:100%;max-height:18rem;margin:1rem}@media (max-width: 959px){.vp-hero-image{margin:2rem auto}}@media (max-width: 719px){.vp-hero-image{max-height:16rem;margin:1.5rem auto}}@media (max-width: 419px){.vp-hero-image{max-height:14rem}}.vp-hero-image.light{display:block}html[data-theme=dark] .vp-hero-image.light,.vp-hero-image.dark{display:none}html[data-theme=dark] .vp-hero-image.dark{display:block}#main-title{margin:.5rem 0;background:linear-gradient(120deg,var(--theme-color-light),var(--theme-color) 30%,#6229b9 100%);-webkit-background-clip:text;background-clip:text;font-weight:700;font-size:3.6rem;font-family:var(--font-family);line-height:1.5;-webkit-text-fill-color:transparent}@media (max-width: 719px){#main-title{margin:0}}@media (max-width: 959px){#main-title{font-size:2.5rem;text-align:center}}@media (max-width: 719px){#main-title{font-size:2.25rem;text-align:center}}@media (max-width: 419px){#main-title{margin:0 auto;font-size:2rem}}#main-description,.vp-hero-actions{margin:1.8rem 0}@media (max-width: 719px){#main-description,.vp-hero-actions{margin:1.5rem 0}}@media (max-width: 959px){#main-description,.vp-hero-actions{margin:1.5rem auto;text-align:center}}@media (max-width: 419px){#main-description,.vp-hero-actions{margin:1.2rem 0}}#main-description{max-width:35rem;color:var(--text-color-light);font-weight:500;font-size:1.6rem;line-height:1.3}@media (max-width: 719px){#main-description{font-size:1.4rem}}@media (max-width: 419px){#main-description{font-size:1.2rem}}.vp-hero-action{display:inline-block;overflow:hidden;min-width:4rem;margin:.5rem;padding:.5em 1.5rem;border-radius:2rem;background:var(--bg-color-secondary);color:var(--text-color);font-size:1.2rem;text-align:center;transition:color var(--color-transition),color var(--color-transition),transform var(--transform-transition)}@media (max-width: 719px){.vp-hero-action{padding:.5rem 1rem;font-size:1.1rem}}@media (max-width: 419px){.vp-hero-action{font-size:1rem}}@media print{.vp-hero-action{text-decoration:none}}.vp-hero-action:hover{background:var(--bg-color-tertiary)}.vp-hero-action.primary{border-color:var(--theme-color);background:var(--theme-color);color:var(--white)}.vp-hero-action.primary:hover{border-color:var(--theme-color-light);background:var(--theme-color-light)}.vp-project-home:not(.pure) .vp-hero-action:active{transform:scale(.96)}.vp-hero-action .icon{margin-inline-end:.25em}.vp-highlight-wrapper{position:relative;display:flex;align-items:center;justify-content:center}.vp-highlight-wrapper:nth-child(2n) .vp-highlight{flex-direction:row-reverse}.vp-highlight{z-index:1;display:flex;flex:1;align-items:center;justify-content:flex-end;max-width:var(--home-page-width);margin:0 auto;padding:1.5rem 2.5rem;color:#222}@media (max-width: 719px){.vp-highlight{display:block;padding-inline:1.5rem;text-align:center}}html[data-theme=dark] .vp-highlight{color:#eee}.vp-highlight-bg{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;background-attachment:fixed;background-position:50%;background-size:cover}.vp-highlight-bg.light{display:inline-block}.vp-highlight-bg.dark,html[data-theme=dark] .vp-highlight-bg.light{display:none}html[data-theme=dark] .vp-highlight-bg.dark{display:inline-block}.vp-highlight-image{width:12rem;margin:2rem 4rem}@media (max-width: 959px){.vp-highlight-image{width:10rem}}@media (max-width: 719px){.vp-highlight-image{width:8rem;margin:0 auto}}.vp-highlight-image.light{display:inline-block}.vp-highlight-image.dark,html[data-theme=dark] .vp-highlight-image.light{display:none}html[data-theme=dark] .vp-highlight-image.dark{display:inline-block}.vp-highlight-info-wrapper{display:flex;flex:1;justify-content:center;padding:2rem}@media (max-width: 719px){.vp-highlight-info-wrapper{padding:1rem 0}}.vp-highlight-info-wrapper:only-child{flex:1 0 100%}.vp-highlight-info{text-align:start}.vp-highlight-header{margin-bottom:1.5rem;border-bottom:none;font-size:3rem;font-family:var(--font-family)}@media (max-width: 959px){.vp-highlight-header{font-size:2.5rem}}@media (max-width: 719px){.vp-highlight-header{font-size:2.25rem;text-align:center}}@media (max-width: 419px){.vp-highlight-header{font-size:2rem}}.vp-highlight-description{font-size:1.125rem}.vp-highlights{margin-inline-start:-1.25em;padding-inline-start:0}.vp-highlight-item-wrapper{padding:.5em .5em .5em 1.75em;border-radius:.5rem;list-style:none}.vp-highlight-item-wrapper.link{cursor:pointer}.vp-highlight-item-wrapper:hover{background-color:var(--bg-color-secondary);box-shadow:0 2px 12px 0 var(--card-shadow);transition:transform var(--transform-transition);transform:translate(-2px,-2px)}.vp-highlight-bg+.vp-highlight .vp-highlight-item-wrapper:hover{background-color:transparent}.vp-highlight-item-wrapper::marker{font-weight:700}.vp-highlight-item{display:list-item;color:inherit;list-style:initial}@media print{.vp-highlight-item{text-decoration:none}}.vp-highlight-title{margin:0;font-weight:600;font-size:1.125rem;font-family:var(--font-family)}.vp-highlight-title .icon{margin-inline-end:.25em;font-size:1em}.vp-highlight-title img.icon{vertical-align:-.125em;height:1em}.vp-highlight-details{margin:.5rem 0 0}.vp-project-home{--content-width: var(--home-page-width);display:block;flex:1;padding-top:var(--navbar-height)}@media screen{.vp-project-home .vp-hero-info-wrapper:not(.fullscreen) .vp-hero-info{max-width:var(--home-page-width)}}@media screen{.vp-project-home .vp-feature{max-width:var(--home-page-width)}}.vp-project-home .theme-hope-content{padding-bottom:1.5rem!important}.vp-project-home .theme-hope-content:empty{padding:0!important}.not-found-hint{padding:2rem}.not-found-hint .error-code{margin:0;font-weight:700;font-size:4rem;line-height:4rem}.not-found-hint .error-title{font-weight:700}.not-found-hint .error-hint{margin:0;padding:12px 0;font-weight:600;font-size:20px;line-height:20px;letter-spacing:2px}.vp-page.not-found{display:flex;flex-direction:column;align-items:center;justify-content:center;box-sizing:border-box;width:100vw;max-width:var(--home-page-width);margin:0 auto;padding:calc(var(--navbar-height) + 1rem) 1rem 1rem!important;text-align:center}.vp-page.not-found .action-button{display:inline-block;box-sizing:border-box;margin:.25rem;padding:.75rem 1rem;border-width:0;border-bottom:1px solid var(--theme-color-dark);border-radius:3rem;background:var(--theme-color);color:var(--white);outline:none;font-size:1rem;transition:background .1s ease}.vp-page.not-found .action-button:hover{background:var(--theme-color-light);cursor:pointer}.vp-page-nav{display:flex;flex-wrap:wrap;min-height:2rem;margin-top:0;padding-block:.5rem;padding-inline:2rem;border-top:1px solid var(--border-color);transition:border-top var(--color-transition)}@media (max-width: 959px){.vp-page-nav{padding-inline:1rem}}@media print{.vp-page-nav{display:none}}.vp-page-nav .nav-link{display:inline-block;flex-grow:1;margin:.25rem;padding:.25rem .5rem;border:1px solid var(--border-color);border-radius:.25rem}.vp-page-nav .nav-link:hover{background:var(--bg-color-secondary)}.vp-page-nav .nav-link .hint{color:var(--light-grey);font-size:.875rem;line-height:2}.vp-page-nav .nav-link .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s;font-size:.75rem}html[data-theme=dark] .vp-page-nav .nav-link .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-page-nav .nav-link .arrow.down{transform:rotate(180deg)}html[dir=rtl] .vp-page-nav .nav-link .arrow.down{transform:rotate(-180deg)}.vp-page-nav .nav-link .arrow.end{transform:rotate(90deg)}html[dir=rtl] .vp-page-nav .nav-link .arrow.end,.vp-page-nav .nav-link .arrow.start{transform:rotate(-90deg)}html[dir=rtl] .vp-page-nav .nav-link .arrow.start{transform:rotate(90deg)}.vp-page-nav .prev{text-align:start}.vp-page-nav .prev .icon{margin-inline-end:.25em;font-size:1em}.vp-page-nav .prev img.icon{vertical-align:-.125em;height:1em}.vp-page-nav .next{text-align:end}.vp-page-nav .next .icon{margin-inline-start:.25em;font-size:1em}.vp-page-nav .next img.icon{vertical-align:-.125em;height:1em}.vp-page-title{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem;position:relative;z-index:1;padding-top:1rem;padding-bottom:0}@media (max-width: 959px){.vp-page-title{padding-inline:1.5rem}}@media print{.vp-page-title{max-width:unset}}@media print{.vp-page-title{padding-inline:0!important}}@media (max-width: 959px){.vp-page-title{padding-top:.5rem}}.vp-page-title h1{margin-top:calc(0px - var(--navbar-height))!important;margin-bottom:1rem;padding-top:var(--navbar-height)!important;font-size:2.2rem}@media (max-width: 959px){.vp-page-title h1{margin-bottom:.5rem}}.vp-page-title h1 .icon{margin-inline-end:.25em;color:var(--theme-color);font-size:.9em}.vp-page-title h1 img.icon{vertical-align:-.125em;height:1em}.theme-hope-content:not(.custom){padding-top:0!important}.theme-hope-content:not(.custom) h1:first-child,.theme-hope-content:not(.custom) h2:first-child,.theme-hope-content:not(.custom) h3:first-child,.theme-hope-content:not(.custom) h4:first-child,.theme-hope-content:not(.custom) h5:first-child,.theme-hope-content:not(.custom) h6:first-child{margin-top:calc(.5rem - var(--navbar-height))!important;padding-top:var(--navbar-height)!important}.theme-hope-content:not(.custom)>h1:first-child{display:none}.vp-page{display:block;flex-grow:1;padding-bottom:2rem;transition:padding var(--transform-transition)}@media print{.vp-page{min-height:auto!important;margin:0!important;padding:0!important}}.page-cover{width:var(--content-width);margin-inline:auto}@media (max-width: 719px){.page-cover{width:100%}}.page-cover img{-o-object-fit:cover;object-fit:cover;width:100%;max-height:25vh;border-radius:.5rem}@media (max-width: 719px){.page-cover img{border-radius:0}}.vp-skip-link{top:.25rem;inset-inline-start:.25rem;z-index:999;padding:.65rem 1.5rem;border-radius:.5rem;background:var(--bg-color);color:var(--theme-color);box-shadow:var(--card-shadow);font-weight:700;font-size:.9em;text-decoration:none}@media print{.vp-skip-link{display:none}}.vp-skip-link:focus{clip:auto;width:auto;height:auto;-webkit-clip-path:none;clip-path:none}.theme-hope-content pre{overflow:auto;margin:.85rem 0;padding:1rem;border-radius:6px;line-height:1.375}.theme-hope-content pre code{padding:0;border-radius:0;background:transparent!important;color:var(--code-color);font-family:var(--font-family-mono);text-align:left;white-space:pre;word-spacing:normal;word-wrap:normal;word-break:normal;overflow-wrap:unset;-webkit-hyphens:none;hyphens:none;transition:color var(--color-transition);-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}@media print{.theme-hope-content pre code{white-space:pre-wrap}}.theme-hope-content .line-number{font-family:var(--font-family-mono)}div[class*=language-]{position:relative;border-radius:6px;background:var(--code-bg-color);font-size:16px;transition:background var(--color-transition)}@media (max-width: 419px){.theme-hope-content>div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}div[class*=language-]:before{content:attr(data-ext);position:absolute;top:0;right:1em;z-index:3;color:var(--code-line-color);font-size:.75rem;transition:color var(--color-transition)}div[class*=language-] pre{position:relative;z-index:1;scrollbar-gutter:stable}div[class*=language-] .highlight-lines{position:absolute;top:0;bottom:0;left:0;width:100%;padding:1rem 0;line-height:1.375;-webkit-user-select:none;-moz-user-select:none;user-select:none}div[class*=language-] .highlight-line{background:var(--code-highlight-line-color);transition:background var(--color-transition)}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;bottom:0;left:0;z-index:2;width:var(--line-numbers-width);border-right:1px solid var(--code-highlight-line-color);border-radius:6px 0 0 6px;transition:border-color var(--color-transition)}@media (max-width: 419px){div[class*=language-].line-numbers-mode:after{border-radius:0}}@media print{div[class*=language-].line-numbers-mode:after{display:none}}div[class*=language-].line-numbers-mode .highlight-line{position:relative}div[class*=language-].line-numbers-mode .highlight-line:before{content:" ";position:absolute;top:0;left:0;z-index:3;display:block;width:var(--line-numbers-width);height:100%}div[class*=language-].line-numbers-mode pre{vertical-align:middle;margin-left:var(--line-numbers-width);padding-left:.5rem}@media print{div[class*=language-].line-numbers-mode pre{margin-left:0;padding-left:1rem}}div[class*=language-].line-numbers-mode .line-numbers{position:absolute;top:0;bottom:0;left:0;display:flex;flex-direction:column;width:var(--line-numbers-width);padding:1rem 0;color:var(--code-line-color);counter-reset:line-number;text-align:center;transition:color var(--color-transition)}@media print{div[class*=language-].line-numbers-mode .line-numbers{display:none}}div[class*=language-].line-numbers-mode .line-number{position:relative;z-index:4;display:flex;flex:1;align-items:center;justify-content:center;-webkit-user-select:none;-moz-user-select:none;user-select:none}div[class*=language-].line-numbers-mode .line-number:before{content:counter(line-number);display:block;font-size:.8em;line-height:1;counter-increment:line-number}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}html[data-theme=light] #app{--code-color: #383a42;--code-line-color: rgba(56, 58, 66, .67);--code-bg-color: #ecf4fa;--code-border-color: #c3def3;--code-highlight-line-color: #d8e9f6}html[data-theme=light] code[class*=language-],html[data-theme=light] pre[class*=language-]{-moz-tab-size:2;-o-tab-size:2;tab-size:2}html[data-theme=light] code[class*=language-]::-moz-selection,html[data-theme=light] code[class*=language-] ::-moz-selection,html[data-theme=light] pre[class*=language-]::-moz-selection,html[data-theme=light] pre[class*=language-] ::-moz-selection{background:#e5e5e6;color:inherit}html[data-theme=light] code[class*=language-]::selection,html[data-theme=light] code[class*=language-] ::selection,html[data-theme=light] pre[class*=language-]::selection,html[data-theme=light] pre[class*=language-] ::selection{background:#e5e5e6;color:inherit}html[data-theme=light] .token.comment,html[data-theme=light] .token.prolog,html[data-theme=light] .token.cdata{color:#a0a1a7}html[data-theme=light] .token.doctype,html[data-theme=light] .token.punctuation,html[data-theme=light] .token.entity{color:#383a42}html[data-theme=light] .token.attr-name,html[data-theme=light] .token.class-name,html[data-theme=light] .token.boolean,html[data-theme=light] .token.constant,html[data-theme=light] .token.number,html[data-theme=light] .token.atrule{color:#b76b01}html[data-theme=light] .token.keyword{color:#a626a4}html[data-theme=light] .token.property,html[data-theme=light] .token.tag,html[data-theme=light] .token.symbol,html[data-theme=light] .token.deleted,html[data-theme=light] .token.important{color:#e45649}html[data-theme=light] .token.selector,html[data-theme=light] .token.string,html[data-theme=light] .token.char,html[data-theme=light] .token.builtin,html[data-theme=light] .token.inserted,html[data-theme=light] .token.regex,html[data-theme=light] .token.attr-value,html[data-theme=light] .token.attr-value>.token.punctuation{color:#50a14f}html[data-theme=light] .token.variable,html[data-theme=light] .token.operator,html[data-theme=light] .token.function{color:#4078f2}html[data-theme=light] .token.url{color:#0184bc}html[data-theme=light] .token.attr-value>.token.punctuation.attr-equals,html[data-theme=light] .token.special-attr>.token.attr-value>.token.value.css{color:#383a42}html[data-theme=light] .language-css .token.selector{color:#e45649}html[data-theme=light] .language-css .token.property{color:#383a42}html[data-theme=light] .language-css .token.function,html[data-theme=light] .language-css .token.url>.token.function{color:#0184bc}html[data-theme=light] .language-css .token.url>.token.string.url{color:#50a14f}html[data-theme=light] .language-css .token.important,html[data-theme=light] .language-css .token.atrule .token.rule,html[data-theme=light] .language-javascript .token.operator{color:#a626a4}html[data-theme=light] .language-javascript .token.template-string>.token.interpolation>.token.interpolation-punctuation.punctuation{color:#ca1243}html[data-theme=light] .language-json .token.operator{color:#383a42}html[data-theme=light] .language-json .token.null.keyword{color:#b76b01}html[data-theme=light] .language-markdown .token.url,html[data-theme=light] .language-markdown .token.url>.token.operator,html[data-theme=light] .language-markdown .token.url-reference.url>.token.string{color:#383a42}html[data-theme=light] .language-markdown .token.url>.token.content{color:#4078f2}html[data-theme=light] .language-markdown .token.url>.token.url,html[data-theme=light] .language-markdown .token.url-reference.url{color:#0184bc}html[data-theme=light] .language-markdown .token.blockquote.punctuation,html[data-theme=light] .language-markdown .token.hr.punctuation{color:#a0a1a7;font-style:italic}html[data-theme=light] .language-markdown .token.code-snippet{color:#50a14f}html[data-theme=light] .language-markdown .token.bold .token.content{color:#b76b01}html[data-theme=light] .language-markdown .token.italic .token.content{color:#a626a4}html[data-theme=light] .language-markdown .token.strike .token.content,html[data-theme=light] .language-markdown .token.strike .token.punctuation,html[data-theme=light] .language-markdown .token.list.punctuation,html[data-theme=light] .language-markdown .token.title.important>.token.punctuation{color:#e45649}html[data-theme=light] .token.bold{font-weight:700}html[data-theme=light] .token.comment,html[data-theme=light] .token.italic{font-style:italic}html[data-theme=light] .token.entity{cursor:help}html[data-theme=light] .token.namespace{opacity:.8}html[data-theme=dark] #app{--code-color: #abb2bf;--code-line-color: rgba(171, 178, 191, .67);--code-bg-color: #282c34;--code-border-color: #343e51;--code-highlight-line-color: #2f3542}html[data-theme=dark] code[class*=language-],html[data-theme=dark] pre[class*=language-]{text-shadow:0 1px rgba(0,0,0,.3);-moz-tab-size:2;-o-tab-size:2;tab-size:2}@media print{html[data-theme=dark] code[class*=language-],html[data-theme=dark] pre[class*=language-]{text-shadow:none}}html[data-theme=dark] code[class*=language-]::-moz-selection,html[data-theme=dark] code[class*=language-] ::-moz-selection,html[data-theme=dark] pre[class*=language-]::-moz-selection,html[data-theme=dark] pre[class*=language-] ::-moz-selection{background:#3e4451;color:inherit;text-shadow:none}html[data-theme=dark] code[class*=language-]::selection,html[data-theme=dark] code[class*=language-] ::selection,html[data-theme=dark] pre[class*=language-]::selection,html[data-theme=dark] pre[class*=language-] ::selection{background:#3e4451;color:inherit;text-shadow:none}html[data-theme=dark] .token.comment,html[data-theme=dark] .token.prolog,html[data-theme=dark] .token.cdata{color:#5c6370}html[data-theme=dark] .token.doctype,html[data-theme=dark] .token.punctuation,html[data-theme=dark] .token.entity{color:#abb2bf}html[data-theme=dark] .token.attr-name,html[data-theme=dark] .token.class-name,html[data-theme=dark] .token.boolean,html[data-theme=dark] .token.constant,html[data-theme=dark] .token.number,html[data-theme=dark] .token.atrule{color:#d19a66}html[data-theme=dark] .token.keyword{color:#c678dd}html[data-theme=dark] .token.property,html[data-theme=dark] .token.tag,html[data-theme=dark] .token.symbol,html[data-theme=dark] .token.deleted,html[data-theme=dark] .token.important{color:#e06c75}html[data-theme=dark] .token.selector,html[data-theme=dark] .token.string,html[data-theme=dark] .token.char,html[data-theme=dark] .token.builtin,html[data-theme=dark] .token.inserted,html[data-theme=dark] .token.regex,html[data-theme=dark] .token.attr-value,html[data-theme=dark] .token.attr-value>.token.punctuation{color:#98c379}html[data-theme=dark] .token.variable,html[data-theme=dark] .token.operator,html[data-theme=dark] .token.function{color:#61afef}html[data-theme=dark] .token.url{color:#56b6c2}html[data-theme=dark] .token.attr-value>.token.punctuation.attr-equals,html[data-theme=dark] .token.special-attr>.token.attr-value>.token.value.css{color:#abb2bf}html[data-theme=dark] .language-css .token.selector{color:#e06c75}html[data-theme=dark] .language-css .token.property{color:#abb2bf}html[data-theme=dark] .language-css .token.function,html[data-theme=dark] .language-css .token.url>.token.function{color:#56b6c2}html[data-theme=dark] .language-css .token.url>.token.string.url{color:#98c379}html[data-theme=dark] .language-css .token.important,html[data-theme=dark] .language-css .token.atrule .token.rule,html[data-theme=dark] .language-javascript .token.operator{color:#c678dd}html[data-theme=dark] .language-javascript .token.template-string>.token.interpolation>.token.interpolation-punctuation.punctuation{color:#be5046}html[data-theme=dark] .language-json .token.operator{color:#abb2bf}html[data-theme=dark] .language-json .token.null.keyword{color:#d19a66}html[data-theme=dark] .language-markdown .token.url,html[data-theme=dark] .language-markdown .token.url>.token.operator,html[data-theme=dark] .language-markdown .token.url-reference.url>.token.string{color:#abb2bf}html[data-theme=dark] .language-markdown .token.url>.token.content{color:#61afef}html[data-theme=dark] .language-markdown .token.url>.token.url,html[data-theme=dark] .language-markdown .token.url-reference.url{color:#56b6c2}html[data-theme=dark] .language-markdown .token.blockquote.punctuation,html[data-theme=dark] .language-markdown .token.hr.punctuation{color:#5c6370;font-style:italic}html[data-theme=dark] .language-markdown .token.code-snippet{color:#98c379}html[data-theme=dark] .language-markdown .token.bold .token.content{color:#d19a66}html[data-theme=dark] .language-markdown .token.italic .token.content{color:#c678dd}html[data-theme=dark] .language-markdown .token.strike .token.content,html[data-theme=dark] .language-markdown .token.strike .token.punctuation,html[data-theme=dark] .language-markdown .token.list.punctuation,html[data-theme=dark] .language-markdown .token.title.important>.token.punctuation{color:#e06c75}html[data-theme=dark] .token.bold{font-weight:700}html[data-theme=dark] .token.comment,html[data-theme=dark] .token.italic{font-style:italic}html[data-theme=dark] .token.entity{cursor:help}html[data-theme=dark] .token.namespace{opacity:.8}.sr-only{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border-width:0;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{.theme-hope-content{margin:0!important;padding-inline:0!important}}.theme-hope-content.custom{margin:0;padding:0}.theme-hope-content:not(.custom){max-width:var(--content-width, 740px);margin:0 auto;padding:2rem 2.5rem;padding-top:0}@media (max-width: 959px){.theme-hope-content:not(.custom){padding:1.5rem}}@media (max-width: 419px){.theme-hope-content:not(.custom){padding:1rem 1.5rem}}@media print{.theme-hope-content:not(.custom){max-width:unset}}.theme-hope-content:not(.custom)>h1,.theme-hope-content:not(.custom)>h2,.theme-hope-content:not(.custom)>h3,.theme-hope-content:not(.custom)>h4,.theme-hope-content:not(.custom)>h5,.theme-hope-content:not(.custom)>h6{margin-top:calc(.5rem - var(--navbar-height));margin-bottom:.5rem;padding-top:calc(1rem + var(--navbar-height));outline:none}.theme-container.no-navbar .theme-hope-content:not(.custom)>h1,.theme-container.no-navbar .theme-hope-content:not(.custom)>h2,.theme-container.no-navbar .theme-hope-content:not(.custom)>h3,.theme-container.no-navbar .theme-hope-content:not(.custom)>h4,.theme-container.no-navbar .theme-hope-content:not(.custom)>h5,.theme-container.no-navbar .theme-hope-content:not(.custom)>h6{margin-top:1.5rem;padding-top:0}.theme-hope-content:not(.custom)>p,.theme-hope-content:not(.custom)>ul p,.theme-hope-content:not(.custom)>ol p{text-align:justify;overflow-wrap:break-word;-webkit-hyphens:auto;hyphens:auto}@media (max-width: 419px){.theme-hope-content:not(.custom)>p,.theme-hope-content:not(.custom)>ul p,.theme-hope-content:not(.custom)>ol p{text-align:start}}@media print{.theme-hope-content:not(.custom)>p,.theme-hope-content:not(.custom)>ul p,.theme-hope-content:not(.custom)>ol p{text-align:start}}.theme-hope-content a:hover{text-decoration:underline}.theme-hope-content img{max-width:100%}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}html[data-theme=light]::view-transition-old(root),html[data-theme=dark]::view-transition-new(root){z-index:1}html[data-theme=light]::view-transition-new(root),html[data-theme=dark]::view-transition-old(root){z-index:99999}@media (min-width: 1280px){.chart-wrapper::-webkit-scrollbar,.flowchart-wrapper::-webkit-scrollbar,.mermaid-wrapper::-webkit-scrollbar{width:8px;height:8px}.chart-wrapper::-webkit-scrollbar-track-piece,.flowchart-wrapper::-webkit-scrollbar-track-piece,.mermaid-wrapper::-webkit-scrollbar-track-piece{border-radius:8px;background:#0000001a}}html[dir=rtl] a.header-anchor{float:right}#docsearch-container{min-width:145.7px!important}@media (max-width: 959px){#docsearch-container{min-width:36px!important}}.DocSearch.DocSearch-Button{margin-left:0}@media (max-width: 959px){.DocSearch.DocSearch-Button{min-width:36px!important}}.DocSearch .DocSearch-Button-Placeholder{display:inline-block;padding:4px 12px 4px 6px;font-size:14px}@media (max-width: 719px){.DocSearch .DocSearch-Button-Placeholder{display:none}}.DocSearch .DocSearch-Search-Icon{width:1.25em;height:1.25em}@media (max-width: 959px){.DocSearch .DocSearch-Button-Keys{display:none}}.DocSearch .DocSearch-Button-Key{background:var(--bg-color);box-shadow:none}:root{scrollbar-width:thin}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track-piece{border-radius:6px;background:#0000001a}::-webkit-scrollbar-thumb{border-radius:6px;background:var(--theme-color)}::-webkit-scrollbar-thumb:active{background:var(--theme-color-light)}@media (max-width: 719px){.hide-in-mobile{display:none!important}}@media (max-width: 959px){.hide-in-pad{display:none!important}}.page-author-item{display:inline-block;margin:0 4px;font-weight:400;overflow-wrap:break-word}.page-category-info{flex-wrap:wrap}.page-category-item{display:inline-block;margin:.125em .25em;padding:0 .25em;border-radius:.25em;background:var(--bg-color-secondary);color:var(--text-color-light);font-weight:700;font-size:.75rem;line-height:2;transition:background var(--color-transition),color var(--color-transition)}@media print{.page-category-item{padding:0;font-weight:400}.page-category-item:after{content:", "}.page-category-item:last-of-type:after{content:""}}.page-category-item.clickable>span:hover{color:var(--theme-color);cursor:pointer}.page-category-item.category0{background:#fde5e7;color:#ec2f3e}html[data-theme=dark] .page-category-item.category0{background:#340509;color:#ba111f}.page-category-item.category0:hover{background:#f9bec3}html[data-theme=dark] .page-category-item.category0:hover{background:#53080e}.page-category-item.category1{background:#ffeee8;color:#fb7649}html[data-theme=dark] .page-category-item.category1{background:#441201;color:#f54205}.page-category-item.category1:hover{background:#fed4c6}html[data-theme=dark] .page-category-item.category1:hover{background:#6d1d02}.page-category-item.category2{background:#fef5e7;color:#f5b041}html[data-theme=dark] .page-category-item.category2{background:#3e2703;color:#e08e0b}.page-category-item.category2:hover{background:#fce6c4}html[data-theme=dark] .page-category-item.category2:hover{background:#633f05}.page-category-item.category3{background:#eafaf1;color:#55d98d}html[data-theme=dark] .page-category-item.category3{background:#0c331c;color:#29b866}.page-category-item.category3:hover{background:#caf3db}html[data-theme=dark] .page-category-item.category3:hover{background:#12522d}.page-category-item.category4{background:#e6f9ee;color:#36d278}html[data-theme=dark] .page-category-item.category4{background:#092917;color:#219552}.page-category-item.category4:hover{background:#c0f1d5}html[data-theme=dark] .page-category-item.category4:hover{background:#0f4224}.page-category-item.category5{background:#e1fcfc;color:#16e1e1}html[data-theme=dark] .page-category-item.category5{background:#042929;color:#0e9595}.page-category-item.category5:hover{background:#b4f8f8}html[data-theme=dark] .page-category-item.category5:hover{background:#064242}.page-category-item.category6{background:#e4f0fe;color:#2589f6}html[data-theme=dark] .page-category-item.category6{background:#021b36;color:#0862c3}.page-category-item.category6:hover{background:#bbdafc}html[data-theme=dark] .page-category-item.category6:hover{background:#042c57}.page-category-item.category7{background:#f7f1fd;color:#bb8ced}html[data-theme=dark] .page-category-item.category7{background:#2a0b4b;color:#9851e4}.page-category-item.category7:hover{background:#eadbfa}html[data-theme=dark] .page-category-item.category7:hover{background:#431277}.page-category-item.category8{background:#fdeaf5;color:#ef59ab}html[data-theme=dark] .page-category-item.category8{background:#400626;color:#e81689}.page-category-item.category8:hover{background:#facbe5}html[data-theme=dark] .page-category-item.category8:hover{background:#670a3d}.page-original-info{position:relative;display:inline-block;vertical-align:middle;overflow:hidden;padding:0 .5em;border:.5px solid var(--dark-grey);border-radius:.75em;background:var(--bg-color);font-size:.75em;line-height:1.5!important}.page-info{display:flex;flex-wrap:wrap;align-items:center;place-content:stretch flex-start;color:var(--dark-grey);font-size:14px}@media print{.page-info{display:flex!important}}.page-info>span{display:flex;align-items:center;max-width:100%;margin-inline-end:.5em;line-height:2}@media (min-width: 1440px){.page-info>span{font-size:1.1em}}@media (max-width: 419px){.page-info>span{margin-inline-end:.3em;font-size:.875em}}@media print{.page-info>span{display:flex!important}}.page-info .icon{position:relative;display:inline-block;vertical-align:middle;width:1em;height:1em;margin-inline-end:.25em}.page-info a{color:inherit}.page-info a:hover,.page-info a:active{color:var(--theme-color)}.page-meta{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem;display:flex;flex-wrap:wrap;justify-content:space-between;overflow:auto;padding-top:.75rem;padding-bottom:.75rem}@media (max-width: 959px){.page-meta{padding-inline:1.5rem}}@media print{.page-meta{max-width:unset}}@media print{.page-meta{margin:0!important;padding-inline:0!important}}@media (max-width: 719px){.page-meta{display:block}}.page-meta .meta-item{flex-grow:1}.page-meta .meta-item .label{font-weight:500}.page-meta .meta-item .label:not(a){color:var(--text-color-lighter)}.page-meta .meta-item .info{color:var(--dark-grey);font-weight:400}.page-meta .git-info{text-align:end}.page-meta .edit-link{margin-top:.25rem;margin-bottom:.25rem;margin-inline-end:.5rem;font-size:14px}@media print{.page-meta .edit-link{display:none}}.page-meta .edit-link .icon{position:relative;bottom:-.125em;width:1em;height:1em;margin-inline-end:.25em}.page-meta .update-time,.page-meta .contributors{margin-top:.25rem;margin-bottom:.25rem;font-size:14px}@media (max-width: 719px){.page-meta .update-time,.page-meta .contributors{font-size:13px;text-align:start}}.print-button{border-width:0;background:transparent;cursor:pointer;box-sizing:content-box;width:1rem;height:1rem;padding:.5rem;border-radius:.25em;color:inherit;font-size:1rem;transform:translateY(.25rem)}@media print{.print-button{display:none}}.page-tag-info{flex-wrap:wrap}.page-tag-item{position:relative;display:inline-block;vertical-align:middle;overflow:hidden;min-width:1.5rem;margin:.125rem;padding:.125rem .25rem .125rem .625rem;background:var(--bg-color-secondary);background:linear-gradient(135deg,transparent .75em,var(--bg-color-secondary) 0) top,linear-gradient(45deg,transparent .75em,var(--bg-color-secondary) 0) bottom;background-size:100% 52%!important;background-repeat:no-repeat!important;color:var(--text-color-light);font-weight:700;font-size:.625rem;line-height:1.5;text-align:center;transition:background var(--color-transition),color var(--color-transition)}@media print{.page-tag-item{padding:0;font-weight:400}.page-tag-item:after{content:", "}.page-tag-item:last-of-type:after{content:""}}.page-tag-item.clickable:hover{cursor:pointer}.page-tag-item.tag0{background:#fde5e7;background:linear-gradient(135deg,transparent .75em,#fde5e7 0) top,linear-gradient(45deg,transparent .75em,#fde5e7 0) bottom;color:#ec2f3e}html[data-theme=dark] .page-tag-item.tag0{background:#340509;background:linear-gradient(135deg,transparent .75em,#340509 0) top,linear-gradient(45deg,transparent .75em,#340509 0) bottom;color:#ba111f}.page-tag-item.tag0.clickable:hover{background:#f9bec3;background:linear-gradient(135deg,transparent .75em,#f9bec3 0) top,linear-gradient(45deg,transparent .75em,#f9bec3 0) bottom}html[data-theme=dark] .page-tag-item.tag0.clickable:hover{background:#53080e;background:linear-gradient(135deg,transparent .75em,#53080e 0) top,linear-gradient(45deg,transparent .75em,#53080e 0) bottom}.page-tag-item.tag1{background:#ffeee8;background:linear-gradient(135deg,transparent .75em,#ffeee8 0) top,linear-gradient(45deg,transparent .75em,#ffeee8 0) bottom;color:#fb7649}html[data-theme=dark] .page-tag-item.tag1{background:#441201;background:linear-gradient(135deg,transparent .75em,#441201 0) top,linear-gradient(45deg,transparent .75em,#441201 0) bottom;color:#f54205}.page-tag-item.tag1.clickable:hover{background:#fed4c6;background:linear-gradient(135deg,transparent .75em,#fed4c6 0) top,linear-gradient(45deg,transparent .75em,#fed4c6 0) bottom}html[data-theme=dark] .page-tag-item.tag1.clickable:hover{background:#6d1d02;background:linear-gradient(135deg,transparent .75em,#6d1d02 0) top,linear-gradient(45deg,transparent .75em,#6d1d02 0) bottom}.page-tag-item.tag2{background:#fef5e7;background:linear-gradient(135deg,transparent .75em,#fef5e7 0) top,linear-gradient(45deg,transparent .75em,#fef5e7 0) bottom;color:#f5b041}html[data-theme=dark] .page-tag-item.tag2{background:#3e2703;background:linear-gradient(135deg,transparent .75em,#3e2703 0) top,linear-gradient(45deg,transparent .75em,#3e2703 0) bottom;color:#e08e0b}.page-tag-item.tag2.clickable:hover{background:#fce6c4;background:linear-gradient(135deg,transparent .75em,#fce6c4 0) top,linear-gradient(45deg,transparent .75em,#fce6c4 0) bottom}html[data-theme=dark] .page-tag-item.tag2.clickable:hover{background:#633f05;background:linear-gradient(135deg,transparent .75em,#633f05 0) top,linear-gradient(45deg,transparent .75em,#633f05 0) bottom}.page-tag-item.tag3{background:#eafaf1;background:linear-gradient(135deg,transparent .75em,#eafaf1 0) top,linear-gradient(45deg,transparent .75em,#eafaf1 0) bottom;color:#55d98d}html[data-theme=dark] .page-tag-item.tag3{background:#0c331c;background:linear-gradient(135deg,transparent .75em,#0c331c 0) top,linear-gradient(45deg,transparent .75em,#0c331c 0) bottom;color:#29b866}.page-tag-item.tag3.clickable:hover{background:#caf3db;background:linear-gradient(135deg,transparent .75em,#caf3db 0) top,linear-gradient(45deg,transparent .75em,#caf3db 0) bottom}html[data-theme=dark] .page-tag-item.tag3.clickable:hover{background:#12522d;background:linear-gradient(135deg,transparent .75em,#12522d 0) top,linear-gradient(45deg,transparent .75em,#12522d 0) bottom}.page-tag-item.tag4{background:#e6f9ee;background:linear-gradient(135deg,transparent .75em,#e6f9ee 0) top,linear-gradient(45deg,transparent .75em,#e6f9ee 0) bottom;color:#36d278}html[data-theme=dark] .page-tag-item.tag4{background:#092917;background:linear-gradient(135deg,transparent .75em,#092917 0) top,linear-gradient(45deg,transparent .75em,#092917 0) bottom;color:#219552}.page-tag-item.tag4.clickable:hover{background:#c0f1d5;background:linear-gradient(135deg,transparent .75em,#c0f1d5 0) top,linear-gradient(45deg,transparent .75em,#c0f1d5 0) bottom}html[data-theme=dark] .page-tag-item.tag4.clickable:hover{background:#0f4224;background:linear-gradient(135deg,transparent .75em,#0f4224 0) top,linear-gradient(45deg,transparent .75em,#0f4224 0) bottom}.page-tag-item.tag5{background:#e1fcfc;background:linear-gradient(135deg,transparent .75em,#e1fcfc 0) top,linear-gradient(45deg,transparent .75em,#e1fcfc 0) bottom;color:#16e1e1}html[data-theme=dark] .page-tag-item.tag5{background:#042929;background:linear-gradient(135deg,transparent .75em,#042929 0) top,linear-gradient(45deg,transparent .75em,#042929 0) bottom;color:#0e9595}.page-tag-item.tag5.clickable:hover{background:#b4f8f8;background:linear-gradient(135deg,transparent .75em,#b4f8f8 0) top,linear-gradient(45deg,transparent .75em,#b4f8f8 0) bottom}html[data-theme=dark] .page-tag-item.tag5.clickable:hover{background:#064242;background:linear-gradient(135deg,transparent .75em,#064242 0) top,linear-gradient(45deg,transparent .75em,#064242 0) bottom}.page-tag-item.tag6{background:#e4f0fe;background:linear-gradient(135deg,transparent .75em,#e4f0fe 0) top,linear-gradient(45deg,transparent .75em,#e4f0fe 0) bottom;color:#2589f6}html[data-theme=dark] .page-tag-item.tag6{background:#021b36;background:linear-gradient(135deg,transparent .75em,#021b36 0) top,linear-gradient(45deg,transparent .75em,#021b36 0) bottom;color:#0862c3}.page-tag-item.tag6.clickable:hover{background:#bbdafc;background:linear-gradient(135deg,transparent .75em,#bbdafc 0) top,linear-gradient(45deg,transparent .75em,#bbdafc 0) bottom}html[data-theme=dark] .page-tag-item.tag6.clickable:hover{background:#042c57;background:linear-gradient(135deg,transparent .75em,#042c57 0) top,linear-gradient(45deg,transparent .75em,#042c57 0) bottom}.page-tag-item.tag7{background:#f7f1fd;background:linear-gradient(135deg,transparent .75em,#f7f1fd 0) top,linear-gradient(45deg,transparent .75em,#f7f1fd 0) bottom;color:#bb8ced}html[data-theme=dark] .page-tag-item.tag7{background:#2a0b4b;background:linear-gradient(135deg,transparent .75em,#2a0b4b 0) top,linear-gradient(45deg,transparent .75em,#2a0b4b 0) bottom;color:#9851e4}.page-tag-item.tag7.clickable:hover{background:#eadbfa;background:linear-gradient(135deg,transparent .75em,#eadbfa 0) top,linear-gradient(45deg,transparent .75em,#eadbfa 0) bottom}html[data-theme=dark] .page-tag-item.tag7.clickable:hover{background:#431277;background:linear-gradient(135deg,transparent .75em,#431277 0) top,linear-gradient(45deg,transparent .75em,#431277 0) bottom}.page-tag-item.tag8{background:#fdeaf5;background:linear-gradient(135deg,transparent .75em,#fdeaf5 0) top,linear-gradient(45deg,transparent .75em,#fdeaf5 0) bottom;color:#ef59ab}html[data-theme=dark] .page-tag-item.tag8{background:#400626;background:linear-gradient(135deg,transparent .75em,#400626 0) top,linear-gradient(45deg,transparent .75em,#400626 0) bottom;color:#e81689}.page-tag-item.tag8.clickable:hover{background:#facbe5;background:linear-gradient(135deg,transparent .75em,#facbe5 0) top,linear-gradient(45deg,transparent .75em,#facbe5 0) bottom}html[data-theme=dark] .page-tag-item.tag8.clickable:hover{background:#670a3d;background:linear-gradient(135deg,transparent .75em,#670a3d 0) top,linear-gradient(45deg,transparent .75em,#670a3d 0) bottom}.toc-place-holder{margin-inline:auto;padding-inline:2.5rem;position:sticky;top:calc(var(--navbar-height) + 2rem);z-index:99;max-width:var(--content-width, 740px)}@media (max-width: 959px){.toc-place-holder{padding-inline:1.5rem}}@media print{.toc-place-holder{max-width:unset}}.toc-place-holder+.theme-hope-content:not(.custom){padding-top:0}#toc{position:absolute;inset-inline-start:calc(100% + 1rem);display:none;min-width:10rem;max-width:15rem}@media (min-width: 1440px){.has-toc #toc{display:block}}@media print{#toc{display:none!important}}#toc .toc-header{margin-bottom:.75rem;margin-inline-start:.5rem;font-weight:600;font-size:.875rem}#toc .toc-wrapper{position:relative;overflow:hidden auto;max-height:75vh;margin:0 .5rem;padding-inline-start:8px;text-overflow:ellipsis;white-space:nowrap;scroll-behavior:smooth}#toc .toc-wrapper::-webkit-scrollbar-track-piece{background:transparent}#toc .toc-wrapper::-webkit-scrollbar{width:3px}#toc .toc-wrapper::-webkit-scrollbar-thumb:vertical{background:#ddd}html[data-theme=dark] #toc .toc-wrapper::-webkit-scrollbar-thumb:vertical{background:#333}#toc .toc-wrapper:before{content:" ";position:absolute;top:0;bottom:0;inset-inline-start:0;z-index:-1;width:2px;background:var(--border-color)}#toc .toc-list{position:relative;margin:0;padding:0}#toc .toc-marker{content:" ";position:absolute;top:0;inset-inline-start:-8px;z-index:2;width:2px;height:1.7rem;background:var(--theme-color);transition:top var(--vp-tt)}#toc .toc-link{position:relative;display:block;overflow:hidden;max-width:100%;color:var(--light-grey);line-height:inherit;text-overflow:ellipsis;white-space:nowrap}#toc .toc-link.level2{padding-inline-start:0px;font-size:14px}#toc .toc-link.level3{padding-inline-start:8px;font-size:13px}#toc .toc-link.level4{padding-inline-start:16px;font-size:12px}#toc .toc-link.level5{padding-inline-start:24px;font-size:11px}#toc .toc-link.level6{padding-inline-start:32px;font-size:10px}#toc .toc-item{position:relative;box-sizing:border-box;height:1.7rem;padding:0 .5rem;list-style:none;line-height:1.7rem}#toc .toc-item:hover>.toc-link{color:var(--theme-color)}#toc .toc-item.active>.toc-link{color:var(--theme-color);font-weight:700}.dropdown-wrapper{cursor:pointer}.dropdown-wrapper:not(:hover) .arrow{transform:rotate(-180deg)}.dropdown-wrapper .dropdown-title{border-width:0;background:transparent;cursor:pointer;padding:0 .25rem;color:var(--dark-grey);font-weight:500;font-size:inherit;font-family:inherit;line-height:inherit;cursor:inherit}.dropdown-wrapper .dropdown-title:hover{border-color:transparent}.dropdown-wrapper .dropdown-title .icon{margin-inline-end:.25em;font-size:1em}.dropdown-wrapper .dropdown-title .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s;font-size:1.2em}html[data-theme=dark] .dropdown-wrapper .dropdown-title .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.dropdown-wrapper .dropdown-title .arrow.down{transform:rotate(180deg)}html[dir=rtl] .dropdown-wrapper .dropdown-title .arrow.down{transform:rotate(-180deg)}.dropdown-wrapper .dropdown-title .arrow.end{transform:rotate(90deg)}html[dir=rtl] .dropdown-wrapper .dropdown-title .arrow.end,.dropdown-wrapper .dropdown-title .arrow.start{transform:rotate(-90deg)}html[dir=rtl] .dropdown-wrapper .dropdown-title .arrow.start{transform:rotate(90deg)}.dropdown-wrapper ul{margin:0;padding:0;list-style-type:none}.dropdown-wrapper .nav-dropdown{position:absolute;top:100%;inset-inline-end:0;overflow-y:auto;box-sizing:border-box;min-width:6rem;max-height:calc(100vh - var(--navbar-height));margin:0;padding:.5rem .75rem;border:1px solid var(--grey14);border-radius:.5rem;background:var(--bg-color);box-shadow:2px 2px 10px var(--card-shadow);text-align:start;white-space:nowrap;opacity:0;visibility:hidden;transition:all .18s ease-out;transform:scale(.9)}.dropdown-wrapper:hover .nav-dropdown,.dropdown-wrapper.open .nav-dropdown{z-index:2;opacity:1;visibility:visible;transform:none}.dropdown-wrapper .nav-link{position:relative;display:block;margin-bottom:0;border-bottom:none;color:var(--dark-grey);font-weight:400;font-size:.875rem;line-height:1.7rem;transition:color var(--color-transition)}.dropdown-wrapper .nav-link:hover,.dropdown-wrapper .nav-link.active{color:var(--theme-color)}.dropdown-wrapper .dropdown-subtitle{margin:0;padding:.5rem .25rem 0;color:var(--light-grey);font-weight:600;font-size:.75rem;line-height:2;text-transform:uppercase;transition:color var(--color-transition)}.dropdown-wrapper .dropdown-subitem-wrapper{padding:0 0 .25rem}.dropdown-wrapper .dropdown-item{color:inherit;line-height:1.7rem}.dropdown-wrapper .dropdown-item:last-child .dropdown-subtitle{padding-top:0}.dropdown-wrapper .dropdown-item:last-child .dropdown-subitem-wrapper{padding-bottom:0}.nav-screen-dropdown-title{border-width:0;background:transparent;position:relative;display:flex;align-items:center;width:100%;padding:0;color:var(--dark-grey);font-size:inherit;font-family:inherit;text-align:start;cursor:pointer}.nav-screen-dropdown-title:hover,.nav-screen-dropdown-title.active{color:var(--text-color)}.nav-screen-dropdown-title .title{flex:1}.nav-screen-dropdown-title .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}html[data-theme=dark] .nav-screen-dropdown-title .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.nav-screen-dropdown-title .arrow.down{transform:rotate(180deg)}html[dir=rtl] .nav-screen-dropdown-title .arrow.down{transform:rotate(-180deg)}.nav-screen-dropdown-title .arrow.end{transform:rotate(90deg)}html[dir=rtl] .nav-screen-dropdown-title .arrow.end,.nav-screen-dropdown-title .arrow.start{transform:rotate(-90deg)}html[dir=rtl] .nav-screen-dropdown-title .arrow.start{transform:rotate(90deg)}.nav-screen-dropdown{overflow:hidden;margin:.5rem 0 0;padding:0;list-style:none;transition:transform .1s ease-out;transform:scaleY(1);transform-origin:top}.nav-screen-dropdown.hide{height:0;margin:0;transform:scaleY(0)}.nav-screen-dropdown .nav-link{position:relative;display:block;padding-inline-start:.5rem;font-weight:400;line-height:2}.nav-screen-dropdown .nav-link:hover,.nav-screen-dropdown .nav-link.active{color:var(--theme-color)}.nav-screen-dropdown .nav-link .icon{font-size:1em}.nav-screen-dropdown .dropdown-item{color:inherit;line-height:1.7rem}.nav-screen-dropdown .dropdown-subtitle{margin:0;padding-inline-start:.25rem;color:var(--light-grey);font-weight:600;font-size:.75rem;line-height:2;text-transform:uppercase;transition:color var(--color-transition)}.nav-screen-dropdown .dropdown-subtitle .nav-link{padding:0}.nav-screen-dropdown .dropdown-subitem-wrapper{margin:0;padding:0;list-style:none}.nav-screen-dropdown .dropdown-subitem{padding-inline-start:.5rem;font-size:.9em}.nav-screen-links{display:none;padding-bottom:.75rem}@media (max-width: 719px){.nav-screen-links{display:block}}.nav-screen-links .navbar-links-item{position:relative;display:block;padding:12px 4px 11px 0;border-bottom:1px solid var(--border-color);font-size:16px;line-height:1.5rem;transition:border-bottom-color var(--color-transition)}.nav-screen-links .nav-link{display:inline-block;width:100%;color:var(--dark-grey);font-weight:400}.nav-screen-links .nav-link:hover{color:var(--text-color)}.nav-screen-links .nav-link.active{color:var(--theme-color)}.vp-nav-screen-container{max-width:320px;margin:0 auto;padding:2rem 0 4rem}#nav-screen{position:fixed;inset:var(--navbar-height) 0 0 0;z-index:150;display:none;overflow-y:auto;padding:0 2rem;background:var(--bg-color);transition:background .5s}@media (max-width: 719px){#nav-screen{display:block}}#nav-screen.fade-enter-active,#nav-screen.fade-leave-active{transition:opacity .25s}#nav-screen.fade-enter-active .vp-nav-screen-container,#nav-screen.fade-leave-active .vp-nav-screen-container{transition:transform .25s ease}#nav-screen.fade-enter-from,#nav-screen.fade-leave-to{opacity:0}#nav-screen.fade-enter-from .vp-nav-screen-container,#nav-screen.fade-leave-to .vp-nav-screen-container{transform:translateY(-8px)}#nav-screen .icon{margin-inline-end:.25em;font-size:1em}#nav-screen img.icon{vertical-align:-.125em;height:1em}.vp-outlook-wrapper{display:flex;justify-content:space-around}.vp-nav-logo{vertical-align:top;height:var(--navbar-line-height);margin-inline-end:.8rem}.vp-nav-logo.light{display:inline-block}.vp-nav-logo.dark,html[data-theme=dark] .vp-nav-logo.light{display:none}html[data-theme=dark] .vp-nav-logo.dark{display:inline-block}.vp-site-name{position:relative;color:var(--text-color);font-size:1.25rem}@media (max-width: 719px){.vp-site-name{overflow:hidden;width:calc(100vw - 9.4rem);text-overflow:ellipsis;white-space:nowrap}}.vp-brand:hover .vp-site-name{color:var(--theme-color)}.vp-navbar .vp-nav-links{display:flex;align-items:center;font-size:.875rem}.vp-navbar .nav-item{position:relative;margin:0 .25rem;line-height:2rem}.vp-navbar .nav-item:first-child{margin-inline-start:0}.vp-navbar .nav-item:last-child{margin-inline-end:0}.vp-navbar .nav-item>.nav-link{color:var(--dark-grey)}.vp-navbar .nav-item>.nav-link:after{content:" ";position:absolute;inset:auto 50% 0;height:2px;border-radius:1px;background:var(--theme-color-light);visibility:hidden;transition:inset .2s ease-in-out}.vp-navbar .nav-item>.nav-link.active{color:var(--theme-color)}.vp-navbar .nav-item>.nav-link:hover:after,.vp-navbar .nav-item>.nav-link.active:after{inset:auto 0 0;visibility:visible}.vp-navbar{--navbar-line-height: calc( var(--navbar-height) - var(--navbar-vertical-padding) * 2 );position:fixed;inset:0 0 auto;z-index:175;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;height:var(--navbar-height);padding:var(--navbar-vertical-padding) var(--navbar-horizontal-padding);background:var(--navbar-bg-color);box-shadow:0 2px 8px var(--card-shadow);line-height:var(--navbar-line-height);white-space:nowrap;transition:transform ease-in-out .3s,background var(--color-transition),box-shadow var(--color-transition);-webkit-backdrop-filter:saturate(150%) blur(12px);backdrop-filter:saturate(150%) blur(12px)}@media print{.vp-navbar{display:none}}.hide-navbar .vp-navbar.auto-hide{transform:translateY(-100%)}.vp-navbar .nav-link{padding:0 .25rem;color:var(--dark-grey)}.vp-navbar .nav-link.active{color:var(--theme-color)}.vp-navbar .nav-link .icon{margin-inline-end:.25em;font-size:1em}.vp-navbar .nav-link img.icon{vertical-align:-.125em;height:1em}.vp-navbar.hide-icon .vp-nav-links .icon{display:none!important}.vp-navbar-start,.vp-navbar-end,.vp-navbar-center{display:flex;flex:1;align-items:center}.vp-navbar-start>*,.vp-navbar-end>*,.vp-navbar-center>*{position:relative;margin:0 .25rem!important}.vp-navbar-start>*:first-child,.vp-navbar-end>*:first-child,.vp-navbar-center>*:first-child{margin-inline-start:0!important}.vp-navbar-start>*:last-child,.vp-navbar-end>*:last-child,.vp-navbar-center>*:last-child{margin-inline-end:0!important}.vp-navbar-start{justify-content:start}.vp-navbar-center{justify-content:center}.vp-navbar-end{justify-content:end}.vp-navbar .vp-repo{margin:0!important}.vp-navbar .vp-repo-link{display:inline-block;margin:auto;padding:6px;color:var(--dark-grey);line-height:1}.vp-navbar .vp-repo-link:hover,.vp-navbar .vp-repo-link:active{color:var(--theme-color)}.vp-toggle-navbar-button{border-width:0;background:transparent;cursor:pointer;position:relative;display:none;align-items:center;justify-content:center;padding:6px}@media screen and (max-width: 719px){.vp-toggle-navbar-button{display:flex}}.vp-toggle-navbar-button>span{position:relative;overflow:hidden;width:16px;height:14px}.vp-toggle-navbar-button .vp-top,.vp-toggle-navbar-button .vp-middle,.vp-toggle-navbar-button .vp-bottom{position:absolute;width:16px;height:2px;background:var(--dark-grey);transition:top .25s,background .5s,transform .25s}.vp-toggle-navbar-button .vp-top{top:0;left:0;transform:translate(0)}.vp-toggle-navbar-button .vp-middle{top:6px;left:0;transform:translate(8px)}.vp-toggle-navbar-button .vp-bottom{top:12px;left:0;transform:translate(4px)}.vp-toggle-navbar-button:hover .vp-top{top:0;left:0;transform:translate(4px)}.vp-toggle-navbar-button:hover .vp-middle{top:6;left:0;transform:translate(0)}.vp-toggle-navbar-button:hover .vp-bottom{top:12px;left:0;transform:translate(8px)}.vp-toggle-navbar-button.is-active .vp-top{top:6px;transform:translate(0) rotate(225deg)}.vp-toggle-navbar-button.is-active .vp-middle{top:6px;transform:translate(16px)}.vp-toggle-navbar-button.is-active .vp-bottom{top:6px;transform:translate(0) rotate(135deg)}.vp-toggle-navbar-button.is-active:hover .vp-top,.vp-toggle-navbar-button.is-active:hover .vp-middle,.vp-toggle-navbar-button.is-active:hover .vp-bottom{background:var(--theme-color);transition:top .25s,background .25s,transform .25s}.vp-toggle-sidebar-button{border-width:0;background:transparent;cursor:pointer;display:none;vertical-align:middle;box-sizing:content-box;width:1rem;height:1rem;padding:.5rem;font:unset;transition:transform .2s ease-in-out}@media screen and (max-width: 719px){.vp-toggle-sidebar-button{display:block;padding-inline-end:var(--navbar-mobile-horizontal-padding)}}.vp-toggle-sidebar-button:before,.vp-toggle-sidebar-button:after,.vp-toggle-sidebar-button .icon{display:block;width:100%;height:2px;border-radius:.05em;background:var(--dark-grey);transition:transform .2s ease-in-out}.vp-toggle-sidebar-button:before{content:" ";margin-top:.125em}.sidebar-open .vp-toggle-sidebar-button:before{transform:translateY(.34rem) rotate(135deg)}.vp-toggle-sidebar-button:after{content:" ";margin-bottom:.125em}.sidebar-open .vp-toggle-sidebar-button:after{transform:translateY(-.34rem) rotate(-135deg)}.vp-toggle-sidebar-button .icon{margin:.2em 0}.sidebar-open .vp-toggle-sidebar-button .icon{transform:scale(0)}.appearance-title{display:block;margin:0;padding:0 .25rem;color:var(--light-grey);font-weight:600;font-size:.75rem;line-height:2;transition:color var(--color-transition)}#appearance-switch{border-width:0;background:transparent;vertical-align:middle;padding:6px;color:var(--dark-grey);cursor:pointer;transition:color var(--color-transition)}#appearance-switch:hover{color:var(--theme-color)}#appearance-switch .icon{width:1.25rem;height:1.25rem}.outlook-button{border-width:0;background:transparent;cursor:pointer;position:relative;padding:.375rem;color:var(--dark-grey)}.outlook-button .icon{vertical-align:middle;width:1.25rem;height:1.25rem}.outlook-dropdown{position:absolute;top:100%;inset-inline-end:0;overflow-y:auto;box-sizing:border-box;min-width:100px;margin:0;padding:.5rem .75rem;border:1px solid var(--grey14);border-radius:.25rem;background:var(--bg-color);box-shadow:2px 2px 10px var(--card-shadow);text-align:start;white-space:nowrap;opacity:0;visibility:hidden;transition:all .18s ease-out;transform:scale(.8)}.outlook-dropdown>*:not(:last-child){padding-bottom:.5rem;border-bottom:1px solid var(--grey14)}.outlook-button:hover .outlook-dropdown,.outlook-button.open .outlook-dropdown{z-index:2;opacity:1;visibility:visible;transform:scale(1)}.theme-color-title{display:block;margin:0;padding:0 .25rem;color:var(--light-grey);font-weight:600;font-size:.75rem;line-height:2;transition:color var(--color-transition)}#theme-color-picker{display:flex;margin:0;padding:0;list-style-type:none;font-size:14px}#theme-color-picker li span{display:inline-block;vertical-align:middle;width:15px;height:15px;margin:0 2px;border-radius:2px}#theme-color-picker li span.theme-color,#theme-color-picker li span.theme-color html[data-theme=dark]{background:#2980b9}:root.theme-1{--theme-color: #2196f3;--theme-color-light: #37a1f4;--theme-color-dark: #0d89ec;--theme-color-mask: rgba(33, 150, 243, .15)}:root.theme-2{--theme-color: #f26d6d;--theme-color-light: #f37c7c;--theme-color-dark: #ef4d4d;--theme-color-mask: rgba(242, 109, 109, .15)}:root.theme-3{--theme-color: #3eaf7c;--theme-color-light: #4abf8a;--theme-color-dark: #389e70;--theme-color-mask: rgba(62, 175, 124, .15)}:root.theme-4{--theme-color: #fb9b5f;--theme-color-light: #fba56f;--theme-color-dark: #fa863d;--theme-color-mask: rgba(251, 155, 95, .15)}@media print{.full-screen-wrapper{display:none}}.full-screen-title{display:block;margin:0;padding:0 .25rem;color:var(--light-grey);font-weight:600;font-size:.75rem;line-height:2;transition:color var(--color-transition)}.full-screen,.cancel-full-screen{border-width:0;background:transparent;vertical-align:middle;padding:.375rem;color:var(--dark-grey);cursor:pointer}.full-screen:hover,.cancel-full-screen:hover{color:var(--theme-color)}.full-screen .icon,.cancel-full-screen .icon{width:1.25rem;height:1.25rem}.enter-fullscreen-icon:hover,.cancel-fullscreen-icon{color:var(--theme-color)}.cancel-fullscreen-icon:hover{color:var(--dark-grey)}.vp-sidebar-heading{display:flex;align-items:center;overflow:hidden;box-sizing:border-box;width:calc(100% - 1rem);margin:0;margin-inline:.5rem;padding:.25rem .5rem;border-width:0;border-radius:.375rem;background:transparent;color:var(--text-color);font-size:1.1em;line-height:1.5;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:color .15s ease;transform:rotate(0)}.vp-sidebar-heading.open{color:inherit}.vp-sidebar-heading.clickable:hover{background:var(--bg-color-secondary)}.vp-sidebar-heading.clickable.exact{border-inline-start-color:var(--theme-color);color:var(--theme-color)}.vp-sidebar-heading.clickable.exact a{color:inherit}.vp-sidebar-heading .vp-sidebar-title{flex:1}.vp-sidebar-heading .vp-arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s;font-size:1.5em}html[data-theme=dark] .vp-sidebar-heading .vp-arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-sidebar-heading .vp-arrow.down{transform:rotate(180deg)}html[dir=rtl] .vp-sidebar-heading .vp-arrow.down{transform:rotate(-180deg)}.vp-sidebar-heading .vp-arrow.end{transform:rotate(90deg)}html[dir=rtl] .vp-sidebar-heading .vp-arrow.end,.vp-sidebar-heading .vp-arrow.start{transform:rotate(-90deg)}html[dir=rtl] .vp-sidebar-heading .vp-arrow.start{transform:rotate(90deg)}button.vp-sidebar-heading{outline:none;font-weight:inherit;font-family:inherit;line-height:inherit;text-align:start;cursor:pointer}.vp-sidebar-link{display:inline-block;box-sizing:border-box;width:calc(100% - 1rem);margin-inline:.5rem;padding:.25rem .5rem;border-radius:.375rem;color:var(--text-color);font-weight:400;font-size:1em;line-height:1.5}.vp-sidebar-link:hover{background:var(--bg-color-secondary)}.vp-sidebar-link.active{background:var(--theme-color-mask);color:var(--theme-color);font-weight:500}.vp-sidebar-link.active .icon{color:var(--theme-color)}.vp-sidebar-sub-headers .vp-sidebar-link{padding-top:.25rem;padding-bottom:.25rem;border-inline-start:none}.vp-sidebar-sub-headers .vp-sidebar-link.active{background:transparent;font-weight:500}.vp-sidebar-group:not(.collapsible) .vp-sidebar-heading:not(.clickable){color:inherit;cursor:auto}.vp-sidebar-group .vp-sidebar-group{padding-inline-start:.75rem}.vp-sidebar-group .vp-sidebar-group .vp-sidebar-heading{font-size:1em}.vp-sidebar-group .vp-sidebar-link{padding-inline-start:1.25rem}.vp-sidebar-links,.vp-sidebar-links ul{margin:0;padding:0}.vp-sidebar-links ul.vp-sidebar-sub-headers{padding-inline-start:.75rem;font-size:.95em}@media (min-width: 1440px){.has-toc .vp-sidebar-links ul.vp-sidebar-sub-headers{display:none}}.vp-sidebar-links li{list-style-type:none}.vp-sidebar>.vp-sidebar-links{padding:1.5rem 0}@media (max-width: 719px){.vp-sidebar>.vp-sidebar-links{padding:1rem 0}}.vp-sidebar>.vp-sidebar-links>li>.vp-sidebar-link{font-size:1.1em}.vp-sidebar>.vp-sidebar-links>li:not(:first-child){margin-top:.5rem}.vp-sidebar{position:fixed;top:0;bottom:0;inset-inline-start:0;z-index:1;overflow-y:auto;width:var(--sidebar-width);margin:0;padding-inline-start:calc(var(--sidebar-space) - var(--sidebar-width));background:var(--sidebar-bg-color);box-shadow:2px 0 8px var(--card-shadow);font-size:.94rem;transition:background var(--color-transition),box-shadow var(--color-transition),padding var(--transform-transition),transform var(--transform-transition);-webkit-backdrop-filter:saturate(150%) blur(12px);backdrop-filter:saturate(150%) blur(12px);scrollbar-color:var(--theme-color) var(--border-color);scrollbar-width:thin}@media (max-width: 959px){.vp-sidebar{font-size:.86em}}@media (max-width: 719px){.vp-sidebar{z-index:125;box-shadow:none;transform:translate(-100%)}html[dir=rtl] .vp-sidebar{transform:translate(100%)}}@media (min-width: 1440px){.vp-sidebar{padding-bottom:3rem;box-shadow:none;font-size:1rem}}@media print{.vp-sidebar{display:none}}.vp-sidebar a{display:inline-block;color:var(--text-color);font-weight:400}.vp-sidebar .icon{margin-inline-end:.25em;font-size:1em}.vp-sidebar img.icon{vertical-align:-.125em;height:1em}.vp-sidebar.hide-icon .icon{display:none!important}.vp-sidebar-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:9;background:#00000026}.vp-sidebar-mask.fade-enter-active,.vp-sidebar-mask.fade-leave-active{transition:opacity .25s}.vp-sidebar-mask.fade-enter-from,.vp-sidebar-mask.fade-leave-to{opacity:0}@media (min-width: 1440px){body{font-size:16px}}:root{--font-family: gitbook-content-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;--font-family-heading: gitbook-content-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif}html[data-theme=dark]{--text-color: #FFFFFF}.theme-hope-content figure{position:relative;display:flex;flex-direction:column;width:auto;margin:1rem auto;text-align:center;transition:transform var(--vp-tt)}.theme-hope-content figure img{overflow:hidden;margin:0 auto;border-radius:8px}.theme-hope-content figure img[tabindex]:hover,.theme-hope-content figure img[tabindex]:focus{box-shadow:2px 2px 10px 0 var(--card-shadow)}@media print{.theme-hope-content figure>a[href^="http://"]:after,.theme-hope-content figure>a[href^="https://"]:after{content:""}}.theme-hope-content figure>a .external-link-icon{display:none}.theme-hope-content figure figcaption{display:inline-block;margin:6px auto;font-size:.8rem}html[data-theme=light] figure:has(img[data-mode=darkmode-only]),html[data-theme=light] img[data-mode=darkmode-only]{display:none!important}html[data-theme=dark] figure:has(img[data-mode=lightmode-only]),html[data-theme=dark] img[data-mode=lightmode-only]{display:none!important}.search-pro-button{border-width:0;background:transparent;display:inline-flex;align-items:center;box-sizing:content-box;height:1.25rem;margin-inline:1rem 0;margin-top:0;margin-bottom:0;padding:.5rem;border:0;border:1px solid var(--vp-bgl);border-radius:1rem;background:var(--vp-bgl);color:var(--vp-c);font-weight:500;cursor:pointer;transition:background var(--vp-ct),color var(--vp-ct)}@media print{.search-pro-button{display:none}}@media (max-width: 959px){.search-pro-button{border-radius:50%}}.search-pro-button:hover{border:1px solid var(--vp-tc);background-color:var(--vp-bglt);color:var(--vp-clt)}.search-pro-button .search-icon{width:1.25rem;height:1.25rem}.search-pro-placeholder{margin-inline:.25rem;font-size:1rem}@media (max-width: 959px){.search-pro-placeholder{display:none}}.search-pro-key-hints{font-size:.75rem}@media (max-width: 959px){.search-pro-key-hints{display:none}}.search-pro-key{display:inline-block;min-width:1em;margin-inline:.125rem;padding:.25rem;border:1px solid var(--vp-brc);border-radius:4px;box-shadow:1px 1px 4px 0 var(--card-shadow);line-height:1;letter-spacing:-.1em;transition:background var(--vp-ct),color var(--vp-ct),border var(--vp-ct) box-shadow var(--vp-ct)}@keyframes search-pro-fade-in{0%{opacity:.2}to{opacity:1}}.search-pro-modal-wrapper{position:fixed;top:0;right:0;bottom:0;left:0;z-index:997;display:flex;align-items:center;justify-content:center;overflow:auto;cursor:default}.search-pro-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:998;animation:.25s search-pro-fade-in;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.search-pro-modal{position:absolute;z-index:999;display:flex;flex-direction:column;width:calc(100% - 6rem);max-width:50em;border-radius:10px;background:var(--vp-bg);box-shadow:2px 2px 10px 0 var(--card-shadow);transition:background var(--vp-ct);animation:.15s pwa-opened}@media (max-width: 1280px){.search-pro-modal{animation:.25s pwa-mobile}}@media (max-width: 719px){.search-pro-modal{width:100vw;max-width:unset;height:100vh}}.search-pro-box{display:flex;margin:1rem}.search-pro-box form{position:relative;display:flex;flex:1}.search-pro-box label{position:absolute;top:calc(50% - .75rem);inset-inline-start:.5rem;color:var(--vp-tc)}.search-pro-box label .search-icon{width:1.5rem;height:1.5rem}.search-pro-clear-button{border-width:0;background:transparent;cursor:pointer;position:absolute;top:calc(50% - 10px);inset-inline-end:.75rem;padding:0;color:var(--vp-tc)}.search-pro-clear-button:hover{border-radius:50%;background-color:#0000001a}.search-pro-close-button{border-width:0;background:transparent;cursor:pointer;display:none;margin-inline:.5rem -.5rem;padding:.5rem;color:var(--grey3);font-size:1rem}@media (max-width: 719px){.search-pro-close-button{display:block}}.search-pro-input{flex:1;width:0;margin:0;padding-block:.25rem;padding-inline:2.5rem 2rem;border:0;border:2px solid var(--vp-tc);border-radius:8px;background:var(--vp-bg);color:var(--vp-c);outline:none;font-size:1.25rem;line-height:2.5;-webkit-appearance:none;-moz-appearance:none;appearance:none}.search-pro-input::-webkit-search-cancel-button{display:none}.search-pro-suggestions{position:absolute;inset:calc(100% + 4px) 0 auto;z-index:20;overflow:visible;overflow-y:auto;max-height:50vh;margin:0;padding:0;border-radius:.5rem;background-color:var(--vp-bg);box-shadow:2px 2px 10px 0 var(--card-shadow);list-style:none;line-height:1.5}.search-pro-suggestion{padding:.25rem 1rem;border-top:1px solid var(--vp-brc);cursor:pointer}.search-pro-suggestion:first-child{border-top:none}.search-pro-suggestion.active,.search-pro-suggestion:hover{background-color:var(--vp-bglt)}.search-pro-auto-complete{display:none;float:right;margin:0 .5rem;padding:4px;border:1px solid var(--vp-brc);border-radius:4px;box-shadow:1px 1px 4px 0 var(--card-shadow);font-size:12px;line-height:1}.search-pro-suggestion.active .search-pro-auto-complete{display:block}.search-pro-result-wrapper{flex-grow:1;overflow-y:auto;min-height:40vh;max-height:calc(80vh - 10rem);padding:0 1rem}@media (max-width: 719px){.search-pro-result-wrapper{min-height:unset;max-height:unset}}.search-pro-result-wrapper.loading,.search-pro-result-wrapper.empty{display:flex;align-items:center;justify-content:center;padding:1.5rem;font-weight:600;font-size:22px;text-align:center}.search-pro-hints{margin-top:1rem;padding:.75rem .5rem;box-shadow:0 -1px 4px 0 var(--card-shadow);line-height:1}.search-pro-hint{display:inline-flex;align-items:center;margin:0 .5rem}.search-pro-hint kbd{margin:0 .5rem;padding:2px;border:1px solid var(--vp-brc);border-radius:4px;box-shadow:1px 1px 4px 0 var(--card-shadow)}.search-pro-hint kbd+kbd{margin-inline-start:-.25rem}.search-pro-hint svg{display:block;width:15px;height:15px}.giscus-wrapper{max-width:var(--content-width, 740px);margin:0 auto;padding:2rem 2.5rem}@media (max-width: 959px){.giscus-wrapper{padding:1.5rem}}@media (max-width: 419px){.giscus-wrapper{padding:1rem 1.5rem}}@media print{.giscus-wrapper{max-width:unset}}@media print{.giscus-wrapper{display:none!important}}.giscus-wrapper.input-top .giscus{margin-bottom:-3rem}.search-pro-result-wrapper{scrollbar-color:var(--vp-tc) var(--vp-brc);scrollbar-width:thin}@media (max-width: 419px){.search-pro-result-wrapper{font-size:14px}}.search-pro-result-wrapper::-webkit-scrollbar{width:6px;height:6px}.search-pro-result-wrapper::-webkit-scrollbar-track-piece{border-radius:6px;background:#0000001a}.search-pro-result-wrapper::-webkit-scrollbar-thumb{border-radius:6px;background:var(--vp-tc)}.search-pro-result-wrapper::-webkit-scrollbar-thumb:active{background:var(--vp-tcl)}.search-pro-result-wrapper mark{border-radius:.25em;line-height:1}.search-pro-result-list{margin:0;padding:0}.search-pro-result-list-item{display:block;list-style:none}.search-pro-result-title{position:sticky;top:-2px;z-index:10;margin:-4px;margin-bottom:.25rem;padding:4px;background:var(--vp-bg);color:var(--vp-tc);font-weight:600;font-size:.85em;line-height:2rem;text-indent:.5em}.search-pro-result-item.active .search-pro-result-title{color:var(--vp-tc)}.search-pro-result-type{display:block;width:1rem;height:1rem;margin-inline-start:-.5rem;padding:.5rem;color:var(--vp-tc)}.search-pro-remove-icon{border-width:0;background:transparent;cursor:pointer;box-sizing:content-box;height:1.5rem;padding:0;border-radius:50%;color:var(--vp-tc);font-size:1rem}.search-pro-remove-icon svg{width:1.5rem;height:1.5rem}.search-pro-remove-icon:hover{background:#8080804d}.search-pro-result-content{display:flex;flex-grow:1;flex-direction:column;align-items:stretch;justify-content:center;line-height:1.5}.search-pro-result-content .content-header{margin-bottom:.25rem;border-bottom:1px solid var(--vp-brcd);font-size:.9em}.search-pro-result-item{display:flex;align-items:center;margin:.5rem 0;padding:.5rem .75rem;border-radius:.25rem;background:var(--vp-bgl);color:inherit;box-shadow:0 1px 3px 0 var(--card-shadow);font-weight:400;white-space:pre-wrap;word-wrap:break-word}.search-pro-result-item strong{color:var(--vp-tc)}.search-pro-result-item:hover,.search-pro-result-item.active{background-color:var(--vp-tcl);color:var(--white);cursor:pointer}.search-pro-result-item:hover .search-pro-result-type,.search-pro-result-item:hover .search-pro-remove-icon,.search-pro-result-item:hover strong,.search-pro-result-item.active .search-pro-result-type,.search-pro-result-item.active .search-pro-remove-icon,.search-pro-result-item.active strong{color:var(--white)} diff --git a/assets/tekton.html-5SA8R3wN.js b/assets/tekton.html-5SA8R3wN.js new file mode 100644 index 00000000..1a761263 --- /dev/null +++ b/assets/tekton.html-5SA8R3wN.js @@ -0,0 +1 @@ +import{_ as t}from"./plugin-vue_export-helper-x3n3nnut.js";import{o,c as a,a as e,d as n}from"./app-cS6i7hsH.js";const r={},c=e("h1",{id:"tekton",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#tekton","aria-hidden":"true"},"#"),n(" tekton")],-1),s=[c];function _(d,i){return o(),a("div",null,s)}const f=t(r,[["render",_],["__file","tekton.html.vue"]]);export{f as default}; diff --git a/assets/tekton.html-7q40Qc0_.js b/assets/tekton.html-7q40Qc0_.js new file mode 100644 index 00000000..4469cff7 --- /dev/null +++ b/assets/tekton.html-7q40Qc0_.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-1a2ca7a7","path":"/devops/tekton.html","title":"tekton","lang":"zh-CN","frontmatter":{"description":"tekton","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/devops/tekton.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"tekton"}],["meta",{"property":"og:description","content":"tekton"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T03:25:58.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T03:25:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"tekton\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T03:25:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706066758000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0,"words":1},"filePathRelative":"devops/tekton.md","localizedDate":"2024年1月24日","excerpt":"如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。
正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.
注意点:(1)不要用自己车辆的邮箱来注册(2)有些邮箱不是特斯拉开发者的邮箱,可能用这些有些无法正常提交访问请求。
点击下方的“提交请求”按钮,请求应用程序访问权限。登录后,请提供您的合法企业详细信息、应用程序名称及描述和使用目的。在您提交了详细信息之后,我们会审核您的请求并通过电子邮件向您发送状态更新。
这一步很坑,多次尝试之后都无果,原因也不知道是为啥,只能自己去看返回报文琢磨,太难受了,下面是自己踩的坑
(1)Invalid domain
无效的域名,这里我用的域名是腾讯云个人服务器的域名,证书是腾讯云免费一年的证书,印象中第一申请的时候还是能过的,第二次的时候就不行了,可能被识别到免费的ssl证书不符合规范,还是需要由合法机构的颁发证书才行。所以,为了金快速申请通过,先填个https://baidu.com吧。当然,后续需要彻底解决自己域名证书的问题,我改为使用阿里云的ssl证书,3个月到期的那种。
(2)Unable to Onboard
应用无法上架,可能原因为邮箱不对,用了之前消费者账号(即自己的车辆账号),建议换别的邮箱试试。
(3) Rejected
这一步尝试了很多次,具体原因为国内还无法正常使用tesla api,只能切换至美国服务器申请下(截止2023-11-15),后续留意官网通知。
一旦获得批准,将为您的应用程序生成可在登录后访问的客户端 ID 和客户端密钥。使用这些凭据,通过 OAuth 2.0 身份验证获取用户访问令牌。访问令牌可用于对提供私人用户账户信息或代表其他账户执行操作的请求进行身份验证。
申请好就可以在自己的账号下看到自己的应用了,
按照 API 文档和设置说明将您的应用程序与 Tesla 车队 API 集成。您需要生成并注册公钥,请求用户授权并按照规格要求拨打电话。完成后您将能够与 API 进行交互,并开始围绕 Tesla 设备构建集成。
由于特斯拉刚推出,并且国内进展缓慢,很多都申请不下来,下面内容均以北美区域进行调用。
app与特斯拉交互的共有两个令牌(token)方式,在调用api的时候,特别需要注意使用的是哪种token,下面是两种token的说明:
(1)合作伙伴身份验证令牌:这个就是你申请的app的令牌
(2)客户生成第三方令牌:这个是消费者在你这个app下授权之后的令牌。
',32),d={href:"https://fleet-api.prd.cn.vn.cloud.tesla.cn/",target:"_blank",rel:"noopener noreferrer"},u=n(`这一步官网列的很详细,就不在详述了。
CLIENT_ID=<command to obtain a client_id>
+CLIENT_SECRET=<secure command to obtain a client_secret>
+AUDIENCE="https://fleet-api.prd.na.vn.cloud.tesla.com"
+# Partner authentication token request
+curl --request POST \\
+ --header 'Content-Type: application/x-www-form-urlencoded' \\
+ --data-urlencode 'grant_type=client_credentials' \\
+ --data-urlencode "client_id=$CLIENT_ID" \\
+ --data-urlencode "client_secret=$CLIENT_SECRET" \\
+ --data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \\
+ --data-urlencode "audience=$AUDIENCE" \\
+ 'https://auth.tesla.com/oauth2/v3/token'
+
+
完成注册合作方账号之后才可以访问API. 每个developer.tesla.cn上的应用程序都必须完成此步骤。官网挂了一个github的代码,看了半天,以为要搞很多东西,实际上只需要几步就可以了。
cd vehicle-command/cmd/tesla-keygen
+go build .
+./tesla-keygen -f -keyring-debug -key-file=private create > public_key.pem
+
这里只是生成了公钥,需要把公钥挂载到域名之下,我们用的是nginx,所以只要指向pem文件就可以了,注意下nginx是否可以访问到改文件,如果不行,把nginx的user改为root。
location ~ ^/.well-known {
+ default_type text/html;
+ alias /root/vehicle-command/cmd/tesla-keygen/public_key.pem;
+ }
+
随后,便是向tesla注册你的域名,域名必须和你申请的时候填的一样。
curl --header 'Content-Type: application/json' \\
+ --header "Authorization: Bearer $TESLA_API_TOKEN" \\
+ --data '{"domain":"string"}' \\
+ 'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts'
+
+
最后,验证一下是否真的注册成功(GET /api/1/partner_accounts/public_key)
curl --header 'Content-Type: application/json' \\
+ --header "Authorization: Bearer $TESLA_API_TOKEN" \\
+ 'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts/public_key'
+
得到下面内容就代表成功了
{"response":{"public_key":"xxxx"}}
+
至此,我们的开发准备就完成了,接下来就是正常的开发与用户交互的api。
`,17);function h(m,v){const s=i("ExternalLinkIcon");return p(),l("div",null,[c,e("p",null,[a("此外,还需要注意下,中国大陆地区对应的api地址是 "),e("a",d,[a("https://fleet-api.prd.cn.vn.cloud.tesla.cn"),o(s)]),a(",不要调到别的地址去了。")]),u])}const k=t(r,[["render",h],["__file","tesla.html.vue"]]);export{k as default}; diff --git a/assets/tesla.html-NVSkMPRC.js b/assets/tesla.html-NVSkMPRC.js new file mode 100644 index 00000000..186a2cf5 --- /dev/null +++ b/assets/tesla.html-NVSkMPRC.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3c114a75","path":"/others/tesla.html","title":"Tesla api","lang":"zh-CN","frontmatter":{"description":"Tesla api 一、特斯拉应用申请 1.1 创建 Tesla 账户 如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。 正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/others/tesla.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Tesla api"}],["meta",{"property":"og:description","content":"Tesla api 一、特斯拉应用申请 1.1 创建 Tesla 账户 如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。 正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Tesla api\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、特斯拉应用申请","slug":"一、特斯拉应用申请","link":"#一、特斯拉应用申请","children":[{"level":3,"title":"1.1 创建 Tesla 账户","slug":"_1-1-创建-tesla-账户","link":"#_1-1-创建-tesla-账户","children":[]},{"level":3,"title":"1.2 提交访问请求","slug":"_1-2-提交访问请求","link":"#_1-2-提交访问请求","children":[]},{"level":3,"title":"1.3 访问应用程序凭据","slug":"_1-3-访问应用程序凭据","link":"#_1-3-访问应用程序凭据","children":[]},{"level":3,"title":"1.4 开始 API 集成","slug":"_1-4-开始-api-集成","link":"#_1-4-开始-api-集成","children":[]},{"level":3,"title":"2.1 认识token","slug":"_2-1-认识token","link":"#_2-1-认识token","children":[]},{"level":3,"title":"2.2 获取第三方应用token","slug":"_2-2-获取第三方应用token","link":"#_2-2-获取第三方应用token","children":[]},{"level":3,"title":"2.3 验证域名归属","slug":"_2-3-验证域名归属","link":"#_2-3-验证域名归属","children":[]}]}],"git":{"createdTime":1706186538000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":4}]},"readingTime":{"minutes":4.5,"words":1351},"filePathRelative":"others/tesla.md","localizedDate":"2024年1月25日","excerpt":"如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。
\\n正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.
\\n\\n","autoDesc":true}');export{e as data}; diff --git a/assets/tiktok2023.html-GIHWTkgC.js b/assets/tiktok2023.html-GIHWTkgC.js new file mode 100644 index 00000000..3e4c69a8 --- /dev/null +++ b/assets/tiktok2023.html-GIHWTkgC.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-091223ce","path":"/interview/tiktok2023.html","title":"","lang":"zh-CN","frontmatter":{"description":"tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决 tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,tlab相关 7.happens-before介绍一下 8.总线风暴是什么,怎么解决 9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决 10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222 11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interview/tiktok2023.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:description","content":"tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决 tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,tlab相关 7.happens-before介绍一下 8.总线风暴是什么,怎么解决 9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决 10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222 11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T03:25:58.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T03:25:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T03:25:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706066758000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":1.15,"words":344},"filePathRelative":"interview/tiktok2023.md","localizedDate":"2024年1月24日","excerpt":"tt一面:\\n1.全程项目\\n2.lc3,最长无重复子串,滑动窗口解决
\\ntt二面:\\n全程基础,一直追问\\n1.java内存模型介绍一下\\n2.volatile原理\\n3.内存屏障,使用场景?(我提了在gc中有使用)\\n4.gc中具体是怎么使用内存屏障的,详细介绍\\n5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么\\n6.线程内存分配方式,tlab相关\\n7.happens-before介绍一下\\n8.总线风暴是什么,怎么解决\\n9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决\\n10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222\\n11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作
","autoDesc":true}');export{e as data}; diff --git a/assets/tiktok2023.html-Ih4GrSaS.js b/assets/tiktok2023.html-Ih4GrSaS.js new file mode 100644 index 00000000..db593f7c --- /dev/null +++ b/assets/tiktok2023.html-Ih4GrSaS.js @@ -0,0 +1 @@ +import{_ as e}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as c,c as o,a as t}from"./app-cS6i7hsH.js";const s={},l=t("p",null,"tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决",-1),n=t("p",null,"tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,tlab相关 7.happens-before介绍一下 8.总线风暴是什么,怎么解决 9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决 10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222 11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作",-1),_=t("p",null,"tt三面: 大部分聊项目,基础随便问了问 1.分布式事务,两阶段三阶段流程,区别 2.mysql主从,同步复制,半同步复制,异步复制 3.算法题,k个一组翻转链表,类似lc25,区别是最后一组不足k个也要翻转 4.场景题,设计一个评论系统,包含发布评论,回复评论,评论点赞等功能,用户过亿,qps百万",-1),a=[l,n,_];function i(r,m){return c(),o("div",null,a)}const f=e(s,[["render",i],["__file","tiktok2023.html.vue"]]);export{f as default}; diff --git a/assets/wewe.html-MSrbCURC.js b/assets/wewe.html-MSrbCURC.js new file mode 100644 index 00000000..4cb90731 --- /dev/null +++ b/assets/wewe.html-MSrbCURC.js @@ -0,0 +1 @@ +import{_ as t}from"./plugin-vue_export-helper-x3n3nnut.js";import{o,c as a,a as e,d as c}from"./app-cS6i7hsH.js";const r={},s=e("h1",{id:"自我介绍",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#自我介绍","aria-hidden":"true"},"#"),c(" 自我介绍")],-1),n=e("p",null,"oifjwoiej",-1),_=[s,n];function d(i,l){return o(),a("div",null,_)}const m=t(r,[["render",d],["__file","wewe.html.vue"]]);export{m as default}; diff --git a/assets/wewe.html-wLcqOgAE.js b/assets/wewe.html-wLcqOgAE.js new file mode 100644 index 00000000..cee953ff --- /dev/null +++ b/assets/wewe.html-wLcqOgAE.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-43e9e6b0","path":"/about-the-author/personal-life/wewe.html","title":"自我介绍","lang":"zh-CN","frontmatter":{"description":"自我介绍 oifjwoiej","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"自我介绍"}],["meta",{"property":"og:description","content":"自我介绍 oifjwoiej"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:53:47.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:53:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"自我介绍\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T04:53:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706244827000,"updatedTime":1706244827000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.02,"words":5},"filePathRelative":"about-the-author/personal-life/wewe.md","localizedDate":"2024年1月26日","excerpt":"oifjwoiej
\\n","autoDesc":true}');export{e as data}; diff --git a/assets/zookeeper.html-YcPoe8IG.js b/assets/zookeeper.html-YcPoe8IG.js new file mode 100644 index 00000000..46357549 --- /dev/null +++ b/assets/zookeeper.html-YcPoe8IG.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-7d9b9318","path":"/middleware/zookeeper/zookeeper.html","title":"Zookeeper","lang":"zh-CN","frontmatter":{"description":"Zookeeper 留空","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Zookeeper"}],["meta",{"property":"og:description","content":"Zookeeper 留空"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T17:34:45.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-25T17:34:45.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Zookeeper\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T17:34:45.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706204085000,"updatedTime":1706204085000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.01,"words":3},"filePathRelative":"middleware/zookeeper/zookeeper.md","localizedDate":"2024年1月25日","excerpt":"留空
\\n","autoDesc":true}');export{e as data}; diff --git a/assets/zookeeper.html-bxhlqtiT.js b/assets/zookeeper.html-bxhlqtiT.js new file mode 100644 index 00000000..08c7cdc1 --- /dev/null +++ b/assets/zookeeper.html-bxhlqtiT.js @@ -0,0 +1 @@ +import{_ as o}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as r,c as t,a as e,d as a}from"./app-cS6i7hsH.js";const c={},s=e("h1",{id:"zookeeper",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#zookeeper","aria-hidden":"true"},"#"),a(" Zookeeper")],-1),n=e("p",null,"留空",-1),_=[s,n];function d(i,l){return r(),t("div",null,_)}const f=o(c,[["render",d],["__file","zookeeper.html.vue"]]);export{f as default}; diff --git "a/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-6moNXOiY.js" "b/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-6moNXOiY.js" new file mode 100644 index 00000000..11dea2eb --- /dev/null +++ "b/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-6moNXOiY.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3f2a15ee","path":"/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html","title":"【elasticsearch】搜索过程详解","lang":"zh-CN","frontmatter":{"description":"【elasticsearch】搜索过程详解 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。 SearchType QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。 DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"【elasticsearch】搜索过程详解"}],["meta",{"property":"og:description","content":"【elasticsearch】搜索过程详解 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。 SearchType QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。 DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:05:46.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:05:46.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"【elasticsearch】搜索过程详解\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T04:05:46.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"SearchType","slug":"searchtype","link":"#searchtype","children":[]},{"level":2,"title":"搜索入口:","slug":"搜索入口","link":"#搜索入口","children":[]},{"level":2,"title":"3.1 query阶段","slug":"_3-1-query阶段","link":"#_3-1-query阶段","children":[]},{"level":2,"title":"3.2 Fetch阶段","slug":"_3-2-fetch阶段","link":"#_3-2-fetch阶段","children":[{"level":3,"title":"3.2.1 FetchSearchPhase(对应上面的1)","slug":"_3-2-1-fetchsearchphase-对应上面的1","link":"#_3-2-1-fetchsearchphase-对应上面的1","children":[]},{"level":3,"title":"3.2.2 ExpandSearchPhase(对应上图的2)","slug":"_3-2-2-expandsearchphase-对应上图的2","link":"#_3-2-2-expandsearchphase-对应上图的2","children":[]}]},{"level":2,"title":"4.1 执行query、fetch流程","slug":"_4-1-执行query、fetch流程","link":"#_4-1-执行query、fetch流程","children":[]}],"git":{"createdTime":1706183164000,"updatedTime":1706241946000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":5}]},"readingTime":{"minutes":8.92,"words":2676},"filePathRelative":"database/elasticsearch/【elasticsearch】搜索过程详解.md","localizedDate":"2024年1月25日","excerpt":"本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。
\\nQUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。\\nDFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。
","autoDesc":true}');export{e as data}; diff --git "a/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-ga4ihhfd.js" "b/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-ga4ihhfd.js" new file mode 100644 index 00000000..45737a80 --- /dev/null +++ "b/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-ga4ihhfd.js" @@ -0,0 +1,300 @@ +import{_ as p}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as c,o,c as i,a as n,d as s,b as t,e}from"./app-cS6i7hsH.js";const u={},l=e(`本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。
QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。 DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。
为了能够深刻了解es的搜索过程,首先创建3个索引,每个索引指定一天的一条记录。
POST aaaa-16/_doc
+{
+ "@timestamp": "2022-02-16T16:21:15.000Z",
+ "word":"16"
+}
+
+
+POST aaaa-17/_doc
+{
+ "@timestamp": "2022-02-17T16:21:15.000Z",
+ "word":"17"
+}
+
+POST aaaa-18/_doc
+{
+ "@timestamp": "2022-02-18T16:21:15.000Z",
+ "word":"18"
+}
+
即可在kibana上看到3条数据
此时,假设我们用一个索引+星号来搜索,es内部的搜索是怎么样的呢?
GET aaaa*/_search
+{
+ "query": {
+ "range": {
+ "@timestamp": {
+ "gte": "2022-02-18T10:21:15.000Z",
+ "lte": "2022-02-18T17:21:15.000Z"
+ }
+ }
+ }
+}
+
正好命中一条记录返回。
{
+ "took" : 2,
+ "timed_out" : false,
+ "_shards" : {
+ "total" : 3,
+ "successful" : 3,
+ "skipped" : 0,
+ "failed" : 0
+ },
+ "hits" : {
+ "total" : {
+ "value" : 1,
+ "relation" : "eq"
+ },
+ "max_score" : 1.0,
+ "hits" : [
+ {
+ "_index" : "aaaa-18",
+ "_id" : "0zB2O38BoMIMP8QzHgdq",
+ "_score" : 1.0,
+ "_source" : {
+ "@timestamp" : "2022-02-18T16:21:15.000Z",
+ "word" : "18"
+ }
+ }
+ ]
+ }
+}
+
+
整个http请求的入口,主要使用的是Netty4HttpRequestHandler。
@ChannelHandler.Sharable
+class Netty4HttpRequestHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest httpRequest) {
+ final Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get();
+ boolean success = false;
+ try {
+ serverTransport.incomingRequest(httpRequest, channel);
+ success = true;
+ } finally {
+ if (success == false) {
+ httpRequest.release();
+ }
+ }
+ }
+}
+
调用链路过程:Netty4HttpRequestHandler.channelRead0->AbstractHttpServerTransport.incomingRequest->AbstractHttpServerTransport.handleIncomingRequest->AbstractHttpServerTransport.dispatchRequest->RestController.dispatchRequest(实现了HttpServerTransport.Dispatcher)->SecurityRestFilter.handleRequest->BaseRestHandler.handleRequest->action.accept(channel)->RestCancellableNodeClient.doExecute->NodeClient.executeLocally->RequestFilterChain.proceed->TransportAction.proceed->TransportSearchAction.doExecute->TransportSearchAction.executeRequest(判断是本地执行还是远程执行)->TransportSearchAction.searchAsyncAction
协调节点的主要功能是接收请求,解析并构建目的的shard列表,然后异步发送到数据节点进行请求查询。具体就不细讲了,可按着debug的来慢慢调试。
特别注意下RestCancellableNodeClient.doExecute,从executeLocally执行所有的查询过程,并注册监听listener.onResponse(response),然后响应。
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(...) {
+ ...
+ TaskHolder taskHolder = new TaskHolder();
+ Task task = client.executeLocally(action, request, new ActionListener<>() {
+ @Override
+ public void onResponse(Response response) {
+ try {
+ closeListener.unregisterTask(taskHolder);
+ } finally {
+ listener.onResponse(response);
+ }
+ }
+ });
+ ...
+}
+
其次要注意的是:TransportSearchAction.searchAsyncAction才开始真正的搜索过程
private SearchPhase searchAsyncAction(...) {
+ ...
+ final QueryPhaseResultConsumer queryResultConsumer = searchPhaseController.newSearchPhaseResults();
+ AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction = switch (searchRequest.searchType()) {
+ case DFS_QUERY_THEN_FETCH -> new SearchDfsQueryThenFetchAsyncAction(...);
+ case QUERY_THEN_FETCH -> new SearchQueryThenFetchAsyncAction(...);
+ };
+ return searchAsyncAction;
+ ...
+}
+
之后就是执行AbstractSearchAsyncAction.start,启动AbstractSearchAsyncAction.executePhase的查询动作。
此处的SearchPhase实现类为SearchQueryThenFetchAsyncAction
+private void executePhase(SearchPhase phase) {
+ try {
+ phase.run();
+ } catch (Exception e) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(new ParameterizedMessage("Failed to execute [{}] while moving to [{}] phase", request, phase.getName()), e);
+ }
+ onPhaseFailure(phase, "", e);
+ }
+}
+
两阶段相应的实现位置:查询(Query)阶段—search.SearchQueryThenFetchAsyncAction;取回(Fetch)阶段—search.FetchSearchPhase。它们都继承自SearchPhase,如下图所示。
(1)客户端发送一个search请求到node3,node3创建一个大小为from,to的优先级队列。 (2)node3转发转发search请求至索引的主分片或者副本,每个分片执行查询请求,并且将结果放到一个排序之后的from、to大小的优先级队列。 (3)每个分片把文档id和排序之后的值返回给协调节点node3,node3把结果合并然后创建一个全局排序之后的结果。
在RestSearchAction#prepareRequest方法中将请求体解析为SearchRequest 数据结构:
+public RestChannelConsumer prepareRequest(.. .) {
+ SearchRequest searchRequest = new SearchRequest();
+ request.withContentOrSourceParamParserOrNull (parser ->
+ parseSearchRequest (searchRequest, request, parser, setSize));
+ ...
+}
+
将请求涉及的本集群shard列表和远程集群的shard列表(远程集群用于跨集群访问)合并:
private void executeSearch(.. .) {
+ ...
+ final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);
+ localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {
+ OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());
+ assert finalIndices != null;
+ return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);
+ }).collect(Collectors.toList());
+ ...
+}
+
查看结果
AbstractSearchAsyncAction.run
+对每个分片进行搜索查询
+for (int i = 0; i < shardsIts.size(); i++) {
+ final SearchShardIterator shardRoutings = shardsIts.get(i);
+ assert shardRoutings.skip() == false;
+ assert shardIndexMap.containsKey(shardRoutings);
+ int shardIndex = shardIndexMap.get(shardRoutings);
+ performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());
+}
+
其中shardsIts是所有aaaa*的所有索引+其中一个副本
AbstractSearchAsyncAction.performPhaseOnShard
+private void performPhaseOnShard(. ..) {
+ executePhaseOnShard(.. .) {
+ //收到执行成功的回复
+ public void inne rOnResponse (FirstResult result) {
+ maybeFork (thread, () -> onShardResult (result,shardIt) );
+ }
+ //收到执行失败的回复
+ public void onFailure (Exception t) {
+ maybeFork(thread, () -> onShardFailure (shardIndex, shard, shard. currentNodeId(),shardIt, t));
+ }
+ });
+}
+
分片结果,当前线程
//AbstractSearchAsyncAction.onShardResult
+...
+private void onShardResult (FirstResult result, SearchShardIterator shardIt) {
+ onShardSuccess(result);
+ success fulShardExecution(shardIt);
+}
+...
+//AbstractSearchAsyncAction.onShardResultConsumed
+private void successfulShardExecution (SearchShardIterator shardsIt) {
+ //计数器累加.
+ final int xTotalOps = totalOps.addAndGet (remainingOpsOnIterator);
+ //检查是否收到全部回复
+ if (xTotalOps == expectedTotalOps) {
+ onPhaseDone ();
+ } else if (xTota1Ops > expectedTotal0ps) {
+ throw new AssertionError(. ..);
+ }
+}
+
+
此处忽略了搜索结果totalHits为0的结果,并将结果进行累加,当xTotalOps等于expectedTotalOps时开始AbstractSearchAsyncAction.onPhaseDone再进行AbstractSearchAsyncAction.executeNextPhase取回阶段
(1)各个shard 返回的只是各文档的id和排序值 IDs and sort values ,coordinate node根据这些id&sort values 构建完priority queue之后,然后把程序需要的document 的id发送mget请求去所有shard上获取对应的document
(2)各个shard将document返回给coordinate node
(3)coordinate node将合并后的document结果返回给client客户端
Query阶段的executeNextPhase方法触发Fetch阶段,Fetch阶段的起点为FetchSearchPhase#innerRun函数,从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch来获取数据,其中包括分页信息。对scroll请求的处理也在FetchSearchPhase#innerRun函数中。
private void innerRun() throws Exception {
+ final int numShards = context.getNumShards();
+ final boolean isScrollSearch = context.getRequest().scroll() != null;
+ final List<SearchPhaseResult> phaseResults = queryResults.asList();
+ final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();
+ final boolean queryAndFetchOptimization = queryResults.length() == 1;
+ final Runnable finishPhase = () -> moveToNextPhase(
+ searchPhaseController,
+ queryResults,
+ reducedQueryPhase,
+ queryAndFetchOptimization ? queryResults : fetchResults.getAtomicArray()
+ );
+ for (int i = 0; i < docIdsToLoad.length; i++) {
+ IntArrayList entry = docIdsToLoad[i];
+ SearchPhaseResult queryResult = queryResults.get(i);
+ if (entry == null) {
+ if (queryResult != null) {
+ releaseIrrelevantSearchContext(queryResult.queryResult());
+ progressListener.notifyFetchResult(i);
+ }
+ counter.countDown();
+ }else{
+ executeFetch(
+ queryResult.getShardIndex(),
+ shardTarget,
+ counter,
+ fetchSearchRequest,
+ queryResult.queryResult(),
+ connection
+ );
+ }
+ }
+ }
+}
+
再看源码:
启动一个线程来fetch
+AbstractSearchAsyncAction.executePhase->FetchSearchPhase.run->FetchSearchPhase.innerRun->FetchSearchPhase.executeFetch
+
+private void executeFetch(...) {
+ context.getSearchTransport()
+ .sendExecuteFetch(
+ connection,
+ fetchSearchRequest,
+ context.getTask(),
+ new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {
+ @Override
+ public void innerOnResponse(FetchSearchResult result) {
+ progressListener.notifyFetchResult(shardIndex);
+ counter.onResult(result);
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ progressListener.notifyFetchFailure(shardIndex, shardTarget, e);
+ counter.onFailure(shardIndex, shardTarget, e);
+ }
+ }
+ );
+}
+
+
counter是一个收集器CountedCollector,onResult(result)主要是每次收到的shard数据存放,并且执行一次countDown,当所有shard数据收集完之后,然后触发一次finishPhase。
# CountedCollector.class
+void countDown() {
+ assert counter.isCountedDown() == false : "more operations executed than specified";
+ if (counter.countDown()) {
+ onFinish.run();
+ }
+}
+
moveToNextPhase方法执行下一阶段,下-阶段要执行的任务定义在FetchSearchPhase构造 函数中,主要是触发ExpandSearchPhase。
AbstractSearchAsyncAction.executePhase->ExpandSearchPhase.run。取回阶段完成之后执行ExpandSearchPhase#run,主要判断是否启用字段折叠,根据需要实现字段折叠功能,如果没有实现字段折叠,则直接返回给客户端。
ExpandSearchPhase执行完之后回复客户端,在AbstractSearchAsyncAction.sendSearchResponse方法中实现:
执行本流程的线程池: search。
对各种Query请求的处理入口注册于SearchTransportService.registerRequestHandler。
public static void registerRequestHandler(TransportService transportService, SearchService searchService) {
+ ...
+ transportService.registerRequestHandler(
+ QUERY_ACTION_NAME,
+ ThreadPool.Names.SAME,
+ ShardSearchRequest::new,
+ (request, channel, task) -> searchService.executeQueryPhase(
+ request,
+ (SearchShardTask) task,
+ new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request)
+ )
+ );
+ ...
+}
+
# SearchService
+public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) {
+ assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1
+ : "empty responses require more than one shard";
+ final IndexShard shard = getShard(request);
+ rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> {
+ ensureAfterSeqNoRefreshed(shard, orig, () -> executeQueryPhase(orig, task), l);
+ }));
+}
+
其中ensureAfterSeqNoRefreshed是把request任务放到一个名为search的线程池里面执行,容量大小为1000。
主要是用来执行SearchService.executeQueryPhase->SearchService.loadOrExecuteQueryPhase->QueryPhase.execute。核心的查询封装在queryPhase.execute(context)中,其中调用Lucene实现检索,同时实现聚合:
public void execute (SearchContext searchContext) {
+ aggregationPhase.preProcess (searchContext);
+ boolean rescore = execute ( searchContext, searchContext.searcher(), searcher::setCheckCancelled, indexSort);
+ if (rescore) {
+ rescorePhase.execute (searchContext);
+ suggestPhase.execute (searchContext);
+ aggregationPhase.execute (searchContext);
+ }
+}
+
其中包含几个核心功能:
对各种Fetch请求的处理入口注册于SearchTransportService.registerRequestHandler。
transportService.registerRequestHandler(
+ QUERY_FETCH_SCROLL_ACTION_NAME,
+ ThreadPool.Names.SAME,
+ InternalScrollSearchRequest::new,
+ (request, channel, task) -> {
+ searchService.executeFetchPhase(
+ request,
+ (SearchShardTask) task,
+ new ChannelActionListener<>(channel, QUERY_FETCH_SCROLL_ACTION_NAME, request)
+ );
+ }
+);
+
对Fetch响应的实现封装在SearchService.executeFetchPhase中,核心是调用fetchPhase.execute(context)。按照命中的doc取得相关数据,填充到SearchHits中,最终封装到FetchSearchResult中。
# FetchPhase
+public void execute(SearchContext context) {
+ Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().startProfilingFetchPhase();
+ SearchHits hits = null;
+ try {
+ //lucene构建搜索的结果
+ hits = buildSearchHits(context, profiler);
+ } finally {
+ ProfileResult profileResult = profiler.finish();
+ // Only set the shardResults if building search hits was successful
+ if (hits != null) {
+ context.fetchResult().shardResult(hits, profileResult);
+ }
+ }
+}
+
入口:RestCancellableNodeClient.doExecute Task task = client.executeLocally主要执行查询,并使用了ActionListener来进行监听
其中onResponse的调用链路如下:RestActionListener.onResponse->RestResponseListener.processResponse->RestController.sendResponse->DefaultRestChannel.sendResponse->Netty4HttpChannel.sendResponse
public void sendResponse(RestResponse restResponse) {
+ ...
+ httpChannel.sendResponse(httpResponse, listener);
+ ...
+}
+
+
最后由Netty4HttpChannel.sendResponse来响应请求。
当我们以aaaa*这样来搜索的时候,实际上查询了所有匹配以aaaa开头的索引,并且对所有的索引的分片都进行了一次Query,再然后对有结果的分片进行一次fetch,最终才能展示结果。可能觉得好奇,对所有分片都进行一次搜索,遍历分片所有的Lucene分段,会不会太过于消耗资源,因此合并Lucene分段对搜索性能有好处,这里下篇文章在讲吧。同时,聚合是发生在fetch过程中的,并不是lucene。
前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。\\n该程序task主要分为三个模块:\\nconsole进行一些cron的配置(表达式、任务名称、任务组等);\\nschedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;\\nclient接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。\\n整体架构跟github上开源的xxl-job类似,也可以参考一下。
","autoDesc":true}');export{e as data}; diff --git "a/assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-ObKNMeJg.js" "b/assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-ObKNMeJg.js" new file mode 100644 index 00000000..b2c1d3f4 --- /dev/null +++ "b/assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-ObKNMeJg.js" @@ -0,0 +1,9 @@ +import{_ as t}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as i,o as r,c as o,a as e,d as a,b as s,e as p}from"./app-cS6i7hsH.js";const l={},c=p(`前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 该程序task主要分为三个模块: console进行一些cron的配置(表达式、任务名称、任务组等); schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发; client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。 整体架构跟github上开源的xxl-job类似,也可以参考一下。
容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n "
+JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
+
其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。
堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。
先从容器中dump出堆内存
jmap -dump:live,format=b,file=heap.hprof 58
+
由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。
另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。 首先是查看堆内存的中占比:
可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:
达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。
声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。
最终解决办法: 在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully
提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了
还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。
第一个想到的就是netty的直接内存,关掉,命令如下:
-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced
+
查看了一下java的nio直接内存,发现也就几十kb,然而直接内存还是慢慢往上涨。毫无头绪,然后开始了自己的从linux层面开始排查问题
一般配合pmap使用,从内核中读取内存块,然后使用views 内存块来判断错误,我简单试了下,乱码,都是二进制的东西,看不出所以然来。
pmap -d 58 | sort -n -k2
+pmap -x 58 | sort -n -k3
+grep rw-p /proc/$1/maps | sed -n 's/^\\([0-9a-f]*\\)-\\([0-9a-f]*\\) .*$/\\1 \\2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done
+
这个时候真的懵了,不知道从何入手了,难道是linux操作系统方面的问题?
起初,在网上看到有人说是因为linux自带的glibc版本太低了,导致的内存溢出,考虑一下。初步觉得也可能是因为这个问题,所以开始慢慢排查。oracle官方有一个jemalloc用来替换linux自带的,谷歌那边也有一个tcmalloc,据说性能比glibc、jemalloc都强,开始换一下。 根据网上说的,在容器里装libunwind,然后再装perf-tools,然后各种捣鼓,到最后发现,执行不了,
pprof --text /usr/bin/java java_58.0001.heap
+
看着工具高大上的,似乎能找出linux的调用栈,
毫无头绪的时候,回想到了linux的top命令以及日志情况,测试环境是由于太多执行端业务方都没有维护,导致调度系统一直会出错,一出错就会导致大量刷错误日志,平均一天一个容器大概就有3G的日志,cron一旦到准点,就会有大量的任务要同时执行,而且容器中是做了对io的限制,磁盘也限制为10G,导致大量的日志都堆积在buff/cache里面,最终直接内存一直在涨,这个时候,系统不会挂,但是先会一直显示内存使用率达到100%。 修复后的结果如下图所示:
定时调度这个系统当时并没有考虑到公司的系统会用的这么多,设计的时候也仅仅是为了实现上千的量,没想到到最后变成了一天的调度都有几百万次。最初那批开发也就使用了大量的本地缓存map来临时存储数据,然后面向简历编程各种用netty自己实现了通信的方式,一堆坑都留给了后人。目前也算是解决掉了一个由于线程过多导致系统不可用的情况而已,但是由于存在大量的map,系统还是得偶尔重启一下比较好。
`,50),d=e("br",null,null,-1),u={href:"https://www.cnblogs.com/testfan2019/p/11151008.html",target:"_blank",rel:"noopener noreferrer"},g=e("br",null,null,-1),h={href:"https://coldwalker.com/2018/08//troubleshooter_native_memory_increase/",target:"_blank",rel:"noopener noreferrer"},m=e("br",null,null,-1),f={href:"https://static.rainfocus.com/oracle/oow18/sess/1524505564465001W0mS/PF/Troubleshooting_native_memory_leaks_1540301908390001k6DR.pdf",target:"_blank",rel:"noopener noreferrer"};function b(v,k){const n=i("ExternalLinkIcon");return r(),o("div",null,[c,e("p",null,[a("参考:"),d,a(" 1."),e("a",u,[a("记一次线上内存泄漏问题的排查过程"),s(n)]),g,a(" 2."),e("a",h,[a("Java堆外内存增长问题排查Case"),s(n)]),m,a(" 3."),e("a",f,[a("Troubleshooting Native Memory Leaks in Java Applications"),s(n)])])])}const y=t(l,[["render",b],["__file","一次jvm调优过程.html.vue"]]);export{y as default}; diff --git "a/assets/\345\206\205\345\255\230\345\261\217\351\232\234.html-P7fE8DoQ.js" "b/assets/\345\206\205\345\255\230\345\261\217\351\232\234.html-P7fE8DoQ.js" new file mode 100644 index 00000000..0eeb9457 --- /dev/null +++ "b/assets/\345\206\205\345\255\230\345\261\217\351\232\234.html-P7fE8DoQ.js" @@ -0,0 +1 @@ +import{_ as t}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as a,c as d,a as e,d as o}from"./app-cS6i7hsH.js";const r={},l=e("h1",{id:"内存屏障",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#内存屏障","aria-hidden":"true"},"#"),o(" 内存屏障")],-1),_=e("p",null,[o("四个内存屏障指令:LoadLoad"),e("code",null,","),o("StoreStore"),e("code",null,","),o("LoadStore"),e("code",null,","),o("StoreLoad")],-1),c=e("p",null,"在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值",-1),n=e("p",null,"在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。",-1),s=[l,_,c,n];function i(h,u){return a(),d("div",null,s)}const p=t(r,[["render",i],["__file","内存屏障.html.vue"]]);export{p as default}; diff --git "a/assets/\345\206\205\345\255\230\345\261\217\351\232\234.html-YpsEGPcG.js" "b/assets/\345\206\205\345\255\230\345\261\217\351\232\234.html-YpsEGPcG.js" new file mode 100644 index 00000000..fa8cee42 --- /dev/null +++ "b/assets/\345\206\205\345\255\230\345\261\217\351\232\234.html-YpsEGPcG.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-660266c1","path":"/java/%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C.html","title":"内存屏障","lang":"zh-CN","frontmatter":{"description":"内存屏障 四个内存屏障指令:LoadLoad,StoreStore,LoadStore,StoreLoad 在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值 在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"内存屏障"}],["meta",{"property":"og:description","content":"内存屏障 四个内存屏障指令:LoadLoad,StoreStore,LoadStore,StoreLoad 在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值 在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T10:30:22.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-24T10:30:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"内存屏障\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T10:30:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706066758000,"updatedTime":1706092222000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.69,"words":208},"filePathRelative":"java/内存屏障.md","localizedDate":"2024年1月24日","excerpt":"四个内存屏障指令:LoadLoad,
StoreStore,
LoadStore,
StoreLoad
在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值
\\n在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。
","autoDesc":true}');export{e as data}; diff --git "a/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-aSy3Jyqy.js" "b/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-aSy3Jyqy.js" new file mode 100644 index 00000000..d89e7e93 --- /dev/null +++ "b/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-aSy3Jyqy.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-90f2343c","path":"/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html","title":"在 Spring 6 中使用虚拟线程","lang":"zh-CN","frontmatter":{"description":"在 Spring 6 中使用虚拟线程 一、简介 在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。 虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"在 Spring 6 中使用虚拟线程"}],["meta",{"property":"og:description","content":"在 Spring 6 中使用虚拟线程 一、简介 在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。 虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"在 Spring 6 中使用虚拟线程\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、简介","slug":"一、简介","link":"#一、简介","children":[]},{"level":2,"title":"二、 虚拟线程与平台线程","slug":"二、-虚拟线程与平台线程","link":"#二、-虚拟线程与平台线程","children":[]},{"level":2,"title":"三、在Spring 6中使用虚拟线程","slug":"三、在spring-6中使用虚拟线程","link":"#三、在spring-6中使用虚拟线程","children":[]},{"level":2,"title":"四、性能比较","slug":"四、性能比较","link":"#四、性能比较","children":[]}],"git":{"createdTime":1706186538000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":4}]},"readingTime":{"minutes":4.83,"words":1448},"filePathRelative":"java/在 Spring 6 中使用虚拟线程.md","localizedDate":"2024年1月25日","excerpt":"在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。
\\n虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。
","autoDesc":true}');export{e as data}; diff --git "a/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-lrPVTD8u.js" "b/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-lrPVTD8u.js" new file mode 100644 index 00000000..d4b14e55 --- /dev/null +++ "b/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-lrPVTD8u.js" @@ -0,0 +1,60 @@ +import{_ as o}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as p,o as c,c as i,a as n,d as a,b as e,e as t}from"./app-cS6i7hsH.js";const l={},r=n("h1",{id:"在-spring-6-中使用虚拟线程",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#在-spring-6-中使用虚拟线程","aria-hidden":"true"},"#"),a(" 在 Spring 6 中使用虚拟线程")],-1),u=n("h2",{id:"一、简介",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#一、简介","aria-hidden":"true"},"#"),a(" 一、简介")],-1),d=n("p",null,"在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。",-1),k={href:"https://openjdk.org/jeps/425",target:"_blank",rel:"noopener noreferrer"},g={href:"https://spring.io/blog/2022/10/11/embracing-virtual-threads",target:"_blank",rel:"noopener noreferrer"},v=n("p",null,"首先,我们将看到“平台线程”和“虚拟线程”之间的主要区别。接下来,我们将使用虚拟线程从头开始构建一个 Spring-Boot 应用程序。最后,我们将创建一个小型测试套件,以查看简单 Web 应用程序吞吐量的最终改进。",-1),m=n("h2",{id:"二、-虚拟线程与平台线程",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#二、-虚拟线程与平台线程","aria-hidden":"true"},"#"),a(" 二、 虚拟线程与平台线程")],-1),h={href:"https://www.baeldung.com/java-virtual-thread-vs-thread",target:"_blank",rel:"noopener noreferrer"},b=n("p",null,"对于本教程来说,必须了解虚拟线程的运行成本远低于平台线程。它们消耗的分配内存量要少得多。这就是为什么可以创建数百万个虚拟线程而不会出现内存不足问题,而不是使用标准平台(或内核)线程创建几百个虚拟线程。",-1),_=n("p",null,"从理论上讲,这赋予了开发人员一种超能力:无需依赖异步代码即可管理高度可扩展的应用程序。",-1),f=n("h2",{id:"三、在spring-6中使用虚拟线程",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#三、在spring-6中使用虚拟线程","aria-hidden":"true"},"#"),a(" 三、在Spring 6中使用虚拟线程")],-1),w={href:"https://www.baeldung.com/java-preview-features",target:"_blank",rel:"noopener noreferrer"},y=n("em",null,"代码",-1),x=t(`<build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>19</source>
+ <target>19</target>
+ <compilerArgs>
+ --enable-preview
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ </plugins>
+</build>
+
从 Java 的角度来看,要使用 Apache Tomcat 和虚拟线程,我们需要一个带有几个 bean 的简单配置类:
@EnableAsync
+@Configuration
+@ConditionalOnProperty(
+ value = "spring.thread-executor",
+ havingValue = "virtual"
+)
+public class ThreadConfig {
+ @Bean
+ public AsyncTaskExecutor applicationTaskExecutor() {
+ return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
+ }
+
+ @Bean
+ public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
+ return protocolHandler -> {
+ protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+ };
+ }
+}
+
spring:
+ thread-executor: virtual
+ //...
+
我们来测试一下Spring Boot应用程序是否使用虚拟线程来处理Web请求调用。为此,我们需要构建一个简单的控制器来返回所需的信息:
@RestController
+@RequestMapping("/thread")
+public class ThreadController {
+ @GetMapping("/name")
+ public String getThreadName() {
+ return Thread.currentThread().toString();
+ }
+}
+
$ curl -s http://localhost:8080/thread/name
+$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4
+
正如我们所看到的,响应明确表示我们正在使用虚拟线程来处理此 Web 请求。换句话说,*Thread.currentThread()*调用返回虚拟线程类的实例。现在让我们通过简单但有效的负载测试来看看虚拟线程的有效性。
在这种特殊的场景中,我们将调用Rest Controller中的一个端点,该端点将简单地让执行休眠一秒钟,模拟复杂的异步任务:
@RestController
+@RequestMapping("/load")
+public class LoadTestController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);
+
+ @GetMapping
+ public void doSomething() throws InterruptedException {
+ LOG.info("hey, I'm doing something");
+ Thread.sleep(1000);
+ }
+}
+
请记住,由于*@ConditionalOnProperty* 注释,我们只需更改 application.yaml 中变量的值即可在虚拟线程和标准线程之间切换。
JMeter 测试将仅包含一个线程组,模拟 1000 个并发用户访问*/load* 端点 100 秒:
在本例中,采用这一新功能所带来的性能提升是显而易见的。让我们比较不同实现的“响应时间图”。这是标准线程的响应图。我们可以看到,立即完成一次调用所需的时间达到 5000 毫秒:
发生这种情况是因为平台线程是一种有限的资源,当所有计划的和池化的线程都忙时,Spring 应用程序除了将请求搁置直到一个线程空闲之外别无选择。
让我们看看虚拟线程会发生什么:
正如我们所看到的,响应稳定在 1000 毫秒。虚拟线程在请求后立即创建和使用,因为从资源的角度来看它们非常便宜。在本例中,我们正在比较 spring 默认固定标准线程池(默认为 200)和 spring 默认无界虚拟线程池的使用情况。
**这种性能提升之所以可能,是因为场景过于简单,并且没有考虑 Spring Boot 应用程序可以执行的全部操作。**从底层操作系统基础设施中采用这种抽象可能是有好处的,但并非在所有情况下都是如此。
`,12);function M(H,N){const s=p("ExternalLinkIcon");return c(),i("div",null,[r,u,d,n("p",null,[a("虚拟线程是Java 19 的"),n("a",k,[a("预览功能"),e(s)]),a(",这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。"),n("a",g,[a("Spring 6 版本"),e(s)]),a("最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。")]),v,m,n("p",null,[a("主要区别在于"),n("a",h,[a("虚拟线程"),e(s)]),a("在其操作周期中不依赖于操作系统线程:它们与硬件解耦,因此有了“虚拟”这个词。这种解耦是由 JVM 提供的抽象层实现的。")]),b,_,f,n("p",null,[a("从 Spring Framework 6(和 Spring Boot 3)开始,虚拟线程功能正式公开,但虚拟线程是Java 19 的"),n("a",w,[a("预览功能。"),e(s)]),a("这意味着我们需要告诉 JVM 我们要在应用程序中启用它们。由于我们使用 Maven 来构建应用程序,因此我们希望确保在 pom.xml 中包含以下"),y,a(":")]),x,n("p",null,[a("第一个 Spring Bean "),T,a("将取代标准的*"),n("a",j,[a("ApplicationTaskExecutor"),e(s)]),a("* ,提供为每个任务启动新虚拟线程的"),E,a("。第二个 bean,名为"),n("em",null,[a("ProtocolHandlerVirtualThreadExecutorCustomizer,"),S,n("a",C,[a("TomcatProtocolHandler 。"),e(s)]),q,n("a",V,[a("@ConditionalOnProperty,"),e(s)]),a("**以通过切换application.yaml")]),a("文件中配置属性的值来按需启用虚拟线程:")]),B,n("p",null,[a("*"),n("a",P,[a("Thread"),e(s)]),a("*对象的toString *()*方法将返回我们需要的所有信息:线程 ID、线程名称、线程组和优先级。让我们通过一个"),n("a",L,[z,e(s)]),a("请求来访问这个端点:")]),A,n("p",null,[a("对于此负载测试,我们将使用"),n("a",I,[a("JMeter"),e(s)]),a("。这不是虚拟线程和标准线程之间的完整性能比较,而是我们可以使用不同参数构建其他测试的起点。")]),J])}const G=o(l,[["render",M],["__file","在 Spring 6 中使用虚拟线程.html.vue"]]);export{G as default}; diff --git "a/assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-FM9j_CKw.js" "b/assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-FM9j_CKw.js" new file mode 100644 index 00000000..10249c31 --- /dev/null +++ "b/assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-FM9j_CKw.js" @@ -0,0 +1,54 @@ +import{_ as t}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as p,o as i,c as o,a as s,d as n,b as e,e as c}from"./app-cS6i7hsH.js";const l={},u=c(`做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。
限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。
限流可以应对:
对于限流场景,一般需要考虑两个维度的信息: 时间 限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定 资源 基于可用资源的限制,比如设定最大访问次数,或最高可用连接数。 限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。
分布式限流相比于单机限流,只是把限流频次分配到各个节点中,比如限制某个服务访问100qps,如果有10个节点,那么每个节点理论上能够平均被访问10次,如果超过了则进行频率限制。
基于Guava的客户端限流 Guava是一个客户端组件,在其多线程模块下提供了以RateLimiter为首的几个限流支持类。它只能对“当前”服务进行限流,即它不属于分布式限流的解决方案。
网关层限流 服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求。我们在网关层进行限流,就可以达到了整体限流的目的了。目前,主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件为代表的F5。
中间件限流 将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。
限流组件 目前也有一些开源组件提供了限流的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中。Hystrix也具有限流的功能。
Guava的Ratelimiter设计实现相当不错,可惜只能支持单机,网关层限流如果是单机则不太满足高可用,并且分布式网关的话还是需要依赖中间件限流,而redis之类的网络通信需要占用一小部分的网络消耗。阿里的Sentinel也是同理,底层使用的是redis或者zookeeper,每次访问都需要调用一次redis或者zk的接口。那么在云原生场景下,我们有没有什么更好的办法呢?
对于极致追求高性能的服务不需要考虑熔断、降级来说,是需要尽量减少网络之间的IO,那么是否可以通过一个总限频然后分配到具体的单机里面去,在单机中实现平均的限流,比如限制某个ip的qps为100,服务总共有10个节点,那么平均到每个服务里就是10qps,此时就可以通过guava的ratelimiter来实现了,甚至说如果服务的节点动态调整,单个服务的qps也能动态调整。
在Spring Boot应用中,定义一个filter,获取请求参数里的key(ip、userId等),然后根据key来获取rateLimiter,其中,rateLimiter的创建由数据库定义的限频数和副本数来判断,最后,再通过rateLimiter.tryAcquire来判断是否可以通过。
在实际的服务中,数据上报服务一般无法确定客户端的上报时间、上报量,特别是对于这种要求高性能,服务一般都会用到HPA来实现动态扩缩容,所以,需要去间隔一段时间去获取服务的副本数。
func CountDeploymentSize(namespace string, deploymentName string) *int32 {
+ deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
+ if err != nil {
+ return nil
+ }
+ return deployment.Spec.Replicas
+}
+
用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。
在RateLimiterService中定义一个LoadingCache<String, RateLimiter>,其中,key可以为ip、userId等,并且,在多线程的情况下,使用refreshAfterWrite只阻塞加载数据的线程,其他线程则返回旧数据,极致发挥缓存的作用。
private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()
+ .maximumSize(10_000)
+ .refreshAfterWrite(20, TimeUnit.MINUTES)
+ .build(this::createRateLimit);
+//定义一个默认最小的QPS
+private static final Integer minQpsLimit = 3000;
+
之后是创建rateLimiter,获取总限频数totalLimit和副本数replicas,之后是自己所需的逻辑判断,可以根据totalLimit和replicas的情况来进行qps的限定。
public RateLimiter createRateLimit(String key) {
+ log.info("createRateLimit,key:{}", key);
+ int totalLimit = 获取总限频数,可以在数据库中定义
+ Integer replicas = kubernetesService.getDeploymentReplicas();
+ RateLimiter rateLimiter;
+ if (totalLimit > 0 && replicas == null) {
+ rateLimiter = RateLimiter.create(totalLimit);
+ } else if (totalLimit > 0) {
+ int nodeQpsLimit = totalLimit / replicas;
+ rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);
+ } else {
+ rateLimiter = RateLimiter.create(minQpsLimit);
+ }
+ log.info("create rateLimiter success,key:{},rateLimiter:{}", key, rateLimiter);
+ return rateLimiter;
+}
+
根据key获取RateLimiter,如果有特殊需求的话,需要判断key不存在的尝尽
public RateLimiter getRateLimiter(String key) {
+ return loadingCache.get(key);
+}
+
最后一步,就是使用rateLimiter来进行限流,如果rateLimiter.tryAcquire()为true,则进行filterChain.doFilter(request, response),如果为false,则返回HttpStatus.TOO_MANY_REQUESTS
public class RateLimiterFilter implements Filter {
+ @Resource
+ private RateLimiterService rateLimiterService;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+ HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+ String key = httpServletRequest.getHeader("key");
+ RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);
+ if (rateLimiter != null) {
+ if (rateLimiter.tryAcquire()) {
+ filterChain.doFilter(request, response);
+ } else {
+ httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
+ }
+ } else {
+ filterChain.doFilter(request, response);
+ }
+ }
+}
+
为了方便对比性能之间的差距,我们在本地单机做了下列测试,其中,总限频都设置为3万。
无限流
使用redis限流
其中,ping redis大概6-7ms左右,对应的,每次请求需要访问redis,时延都有大概6-7ms,性能下降明显
自研限流
性能几乎追平无限流的场景,guava的rateLimiter确实表现卓越
5.1 对于保证qps限频准确的时候,应该怎么解决呢?
在k8s中,服务是动态扩缩容的,相应的,每个节点应该都要有所变化,如果对外宣称限频100qps,而且后续业务方真的要求百分百准确,只能把LoadingCache<String, RateLimiter>的过期时间调小一点,让它能够近实时的更新单节点的qps。这里还需要考虑一下k8s的压力,因为每次都要获取副本数,这里也是需要做缓存的
5.2 服务从1个节点动态扩为4个节点,这个时候新节点识别为4,但其实有些并没有启动完,会不会造成某个节点承受了太大的压力
理论上是存在这个可能的,这个时候需要考虑一下初始的副本数的,扩缩容不能一蹴而就,一下子从1变为4变为几十个这种。一般的话,生产环境肯定是不能只有一个节点,并且要考虑扩缩容的话,至于要有多个副本预备的
5.3 如果有多个副本,怎么保证请求是均匀的
这个是依赖于k8s的service负载均衡策略的,这个我们之前做过实验,流量确实是能够均匀的落到节点上的。还有就是,我们整个限流都是基于k8s的,如果k8s出现问题,那就是整个集群所有服务都有可能出现问题了。
做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。
\\n限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。
\\n限流可以应对:
\\n第一次听到执行计划还是挺懵的,感觉像是存储函数什么样的东西,后来发现,居然是explain
\\n","autoDesc":true}');export{e as data}; diff --git "a/assets/\346\211\247\350\241\214\350\256\241\345\210\222explain.html-y5jML1O_.js" "b/assets/\346\211\247\350\241\214\350\256\241\345\210\222explain.html-y5jML1O_.js" new file mode 100644 index 00000000..f018d8a0 --- /dev/null +++ "b/assets/\346\211\247\350\241\214\350\256\241\345\210\222explain.html-y5jML1O_.js" @@ -0,0 +1 @@ +import{_ as a}from"./plugin-vue_export-helper-x3n3nnut.js";import{o as t,c as n,a as e,d as o}from"./app-cS6i7hsH.js";const _={},c=e("h1",{id:"执行计划explain",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#执行计划explain","aria-hidden":"true"},"#"),o(" 执行计划explain")],-1),r=e("p",null,"第一次听到执行计划还是挺懵的,感觉像是存储函数什么样的东西,后来发现,居然是explain",-1),s=[c,r];function i(l,d){return t(),n("div",null,s)}const x=a(_,[["render",i],["__file","执行计划explain.html.vue"]]);export{x as default}; diff --git "a/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-fj1Jh_qh.js" "b/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-fj1Jh_qh.js" new file mode 100644 index 00000000..dff6e58b --- /dev/null +++ "b/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-fj1Jh_qh.js" @@ -0,0 +1 @@ +import{_ as s}from"./plugin-vue_export-helper-x3n3nnut.js";import{r as o,o as c,c as _,a as e,d as t,b as a}from"./app-cS6i7hsH.js";const r={},l=e("h1",{id:"数据库缓存",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#数据库缓存","aria-hidden":"true"},"#"),t(" 数据库缓存")],-1),d=e("p",null,"在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。",-1),h={href:"https://so.csdn.net/so/search?q=hash&spm=1001.2101.3001.7020",target:"_blank",rel:"noopener noreferrer"},i=e("p",null,"禁用原因:",-1),p=e("p",null,"1.命中率低",-1),m=e("p",null,"2.写时所有都失效",-1),u=e("p",null,"禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。",-1),f=e("p",null,"查询缓存讲解:https://blog.csdn.net/zzddada/article/details/124116182",-1);function x(k,b){const n=o("ExternalLinkIcon");return c(),_("div",null,[l,d,e("p",null,[t("将select语句和语句的结果做"),e("a",h,[t("hash"),a(n)]),t("映射关系后保存在一定的内存区域内。")]),i,p,m,u,f])}const L=s(r,[["render",x],["__file","数据库缓存.html.vue"]]);export{L as default}; diff --git "a/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-k3XJyHqW.js" "b/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-k3XJyHqW.js" new file mode 100644 index 00000000..dd540894 --- /dev/null +++ "b/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-k3XJyHqW.js" @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0a0b7a54","path":"/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html","title":"数据库缓存","lang":"zh-CN","frontmatter":{"description":"数据库缓存 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。 将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。 禁用原因: 1.命中率低 2.写时所有都失效 禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"数据库缓存"}],["meta",{"property":"og:description","content":"数据库缓存 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。 将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。 禁用原因: 1.命中率低 2.写时所有都失效 禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T11:42:16.000Z"}],["meta",{"property":"article:author","content":"Zephery"}],["meta",{"property":"article:modified_time","content":"2024-01-25T11:42:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"数据库缓存\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T11:42:16.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706092222000,"updatedTime":1706182936000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.34,"words":101},"filePathRelative":"database/mysql/数据库缓存.md","localizedDate":"2024年1月24日","excerpt":"在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。
\\n将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。
\\n禁用原因:
\\n1.命中率低
\\n2.写时所有都失效
\\n禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。
","autoDesc":true}');export{e as data}; diff --git a/bigdata/index.html b/bigdata/index.html new file mode 100644 index 00000000..351c493b --- /dev/null +++ b/bigdata/index.html @@ -0,0 +1,46 @@ + + + + + + + + + +Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
(2)数据RDBMS:mysql、oracle、mssql
(3)NOSQL数据库:HBase、ES、Redis
(4)消息对象:Redis
elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。
相关库引入:
<dependency>
+ <groupId>org.elasticsearch</groupId>
+ <artifactId>elasticsearch-spark-30_2.13</artifactId>
+ <version>8.1.3</version>
+ </dependency>
+
SparkConf配置,更多详细的请点击这里或者源码ConfigurationOptions。
public static SparkConf getSparkConf() {
+ SparkConf sparkConf = new SparkConf().setAppName("elasticsearch-spark-demo");
+ sparkConf.set("es.nodes", "host")
+ .set("es.port", "xxxxxx")
+ .set("es.nodes.wan.only", "true")
+ .set("es.net.http.auth.user", "elxxxxastic")
+ .set("es.net.http.auth.pass", "xxxx")
+ .setMaster("local[*]");
+ return sparkConf;
+}
+
这里用的是kibana提供的sample data里面的索引kibana_sample_data_ecommerce,也可以替换成自己的索引。
public static void main(String[] args) {
+ SparkConf conf = getSparkConf();
+ try (JavaSparkContext jsc = new JavaSparkContext(conf)) {
+
+ JavaPairRDD<String, Map<String, Object>> esRDD =
+ JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce");
+ esRDD.collect().forEach(System.out::println);
+ }
+}
+
esRDD同时也支持query语句esRDD(final JavaSparkContext jsc, final String resource, final String query),一般对es的查询都需要根据时间筛选一下,不过相对于es的官方sdk,并没有那么友好的api,只能直接使用原生的dsl语句。
支持序列化对象、json,并且能够使用占位符动态索引写入数据(使用较少),不过多介绍了。
public static void jsonWrite(){
+ String json1 = "{\"reason\" : \"business\",\"airport\" : \"SFO\"}";
+ String json2 = "{\"participants\" : 5,\"airport\" : \"OTP\"}";
+ JavaRDD<String> stringRDD = jsc.parallelize(ImmutableList.of(json1, json2));
+ JavaEsSpark.saveJsonToEs(stringRDD, "spark-json");
+}
+
比较常用的读写也就这些,更多可以看下官网相关介绍。
spark的实时处理,es5.0的时候开始支持,目前
直接用idea下载代码https://github.com/elastic/elasticsearch.git
切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好
为了方便, 在 gradle/run.gradle 中关闭 Auth 认证:
setting 'xpack.security.enabled', 'false'
或者使用其中的用户名密码:
user username: 'elastic-admin', password: 'elastic-password', role: 'superuser'
先启动上面的 remote debug, 然后用 gradlew 启动项目:
./gradlew :run --debug-jvm 打开浏览器http://localhost:9200即可看到es相关信息了
本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。
QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。 DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。
为了能够深刻了解es的搜索过程,首先创建3个索引,每个索引指定一天的一条记录。
POST aaaa-16/_doc
+{
+ "@timestamp": "2022-02-16T16:21:15.000Z",
+ "word":"16"
+}
+
+
+POST aaaa-17/_doc
+{
+ "@timestamp": "2022-02-17T16:21:15.000Z",
+ "word":"17"
+}
+
+POST aaaa-18/_doc
+{
+ "@timestamp": "2022-02-18T16:21:15.000Z",
+ "word":"18"
+}
+
即可在kibana上看到3条数据
此时,假设我们用一个索引+星号来搜索,es内部的搜索是怎么样的呢?
GET aaaa*/_search
+{
+ "query": {
+ "range": {
+ "@timestamp": {
+ "gte": "2022-02-18T10:21:15.000Z",
+ "lte": "2022-02-18T17:21:15.000Z"
+ }
+ }
+ }
+}
+
正好命中一条记录返回。
{
+ "took" : 2,
+ "timed_out" : false,
+ "_shards" : {
+ "total" : 3,
+ "successful" : 3,
+ "skipped" : 0,
+ "failed" : 0
+ },
+ "hits" : {
+ "total" : {
+ "value" : 1,
+ "relation" : "eq"
+ },
+ "max_score" : 1.0,
+ "hits" : [
+ {
+ "_index" : "aaaa-18",
+ "_id" : "0zB2O38BoMIMP8QzHgdq",
+ "_score" : 1.0,
+ "_source" : {
+ "@timestamp" : "2022-02-18T16:21:15.000Z",
+ "word" : "18"
+ }
+ }
+ ]
+ }
+}
+
+
一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。当搜索任务执行在分布式系统上时,整体流程如下图所示。图片来源Elasitcsearch源码解析与优化实战
整个http请求的入口,主要使用的是Netty4HttpRequestHandler。
@ChannelHandler.Sharable
+class Netty4HttpRequestHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest httpRequest) {
+ final Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get();
+ boolean success = false;
+ try {
+ serverTransport.incomingRequest(httpRequest, channel);
+ success = true;
+ } finally {
+ if (success == false) {
+ httpRequest.release();
+ }
+ }
+ }
+}
+
调用链路过程:Netty4HttpRequestHandler.channelRead0->AbstractHttpServerTransport.incomingRequest->AbstractHttpServerTransport.handleIncomingRequest->AbstractHttpServerTransport.dispatchRequest->RestController.dispatchRequest(实现了HttpServerTransport.Dispatcher)->SecurityRestFilter.handleRequest->BaseRestHandler.handleRequest->action.accept(channel)->RestCancellableNodeClient.doExecute->NodeClient.executeLocally->RequestFilterChain.proceed->TransportAction.proceed->TransportSearchAction.doExecute->TransportSearchAction.executeRequest(判断是本地执行还是远程执行)->TransportSearchAction.searchAsyncAction
协调节点的主要功能是接收请求,解析并构建目的的shard列表,然后异步发送到数据节点进行请求查询。具体就不细讲了,可按着debug的来慢慢调试。
特别注意下RestCancellableNodeClient.doExecute,从executeLocally执行所有的查询过程,并注册监听listener.onResponse(response),然后响应。
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(...) {
+ ...
+ TaskHolder taskHolder = new TaskHolder();
+ Task task = client.executeLocally(action, request, new ActionListener<>() {
+ @Override
+ public void onResponse(Response response) {
+ try {
+ closeListener.unregisterTask(taskHolder);
+ } finally {
+ listener.onResponse(response);
+ }
+ }
+ });
+ ...
+}
+
其次要注意的是:TransportSearchAction.searchAsyncAction才开始真正的搜索过程
private SearchPhase searchAsyncAction(...) {
+ ...
+ final QueryPhaseResultConsumer queryResultConsumer = searchPhaseController.newSearchPhaseResults();
+ AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction = switch (searchRequest.searchType()) {
+ case DFS_QUERY_THEN_FETCH -> new SearchDfsQueryThenFetchAsyncAction(...);
+ case QUERY_THEN_FETCH -> new SearchQueryThenFetchAsyncAction(...);
+ };
+ return searchAsyncAction;
+ ...
+}
+
之后就是执行AbstractSearchAsyncAction.start,启动AbstractSearchAsyncAction.executePhase的查询动作。
此处的SearchPhase实现类为SearchQueryThenFetchAsyncAction
+private void executePhase(SearchPhase phase) {
+ try {
+ phase.run();
+ } catch (Exception e) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(new ParameterizedMessage("Failed to execute [{}] while moving to [{}] phase", request, phase.getName()), e);
+ }
+ onPhaseFailure(phase, "", e);
+ }
+}
+
两阶段相应的实现位置:查询(Query)阶段—search.SearchQueryThenFetchAsyncAction;取回(Fetch)阶段—search.FetchSearchPhase。它们都继承自SearchPhase,如下图所示。
图片来源官网,比较旧,但任然可用
(1)客户端发送一个search请求到node3,node3创建一个大小为from,to的优先级队列。 (2)node3转发转发search请求至索引的主分片或者副本,每个分片执行查询请求,并且将结果放到一个排序之后的from、to大小的优先级队列。 (3)每个分片把文档id和排序之后的值返回给协调节点node3,node3把结果合并然后创建一个全局排序之后的结果。
在RestSearchAction#prepareRequest方法中将请求体解析为SearchRequest 数据结构:
+public RestChannelConsumer prepareRequest(.. .) {
+ SearchRequest searchRequest = new SearchRequest();
+ request.withContentOrSourceParamParserOrNull (parser ->
+ parseSearchRequest (searchRequest, request, parser, setSize));
+ ...
+}
+
将请求涉及的本集群shard列表和远程集群的shard列表(远程集群用于跨集群访问)合并:
private void executeSearch(.. .) {
+ ...
+ final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);
+ localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {
+ OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());
+ assert finalIndices != null;
+ return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);
+ }).collect(Collectors.toList());
+ ...
+}
+
查看结果
AbstractSearchAsyncAction.run
+对每个分片进行搜索查询
+for (int i = 0; i < shardsIts.size(); i++) {
+ final SearchShardIterator shardRoutings = shardsIts.get(i);
+ assert shardRoutings.skip() == false;
+ assert shardIndexMap.containsKey(shardRoutings);
+ int shardIndex = shardIndexMap.get(shardRoutings);
+ performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());
+}
+
其中shardsIts是所有aaaa*的所有索引+其中一个副本
AbstractSearchAsyncAction.performPhaseOnShard
+private void performPhaseOnShard(. ..) {
+ executePhaseOnShard(.. .) {
+ //收到执行成功的回复
+ public void inne rOnResponse (FirstResult result) {
+ maybeFork (thread, () -> onShardResult (result,shardIt) );
+ }
+ //收到执行失败的回复
+ public void onFailure (Exception t) {
+ maybeFork(thread, () -> onShardFailure (shardIndex, shard, shard. currentNodeId(),shardIt, t));
+ }
+ });
+}
+
分片结果,当前线程
//AbstractSearchAsyncAction.onShardResult
+...
+private void onShardResult (FirstResult result, SearchShardIterator shardIt) {
+ onShardSuccess(result);
+ success fulShardExecution(shardIt);
+}
+...
+//AbstractSearchAsyncAction.onShardResultConsumed
+private void successfulShardExecution (SearchShardIterator shardsIt) {
+ //计数器累加.
+ final int xTotalOps = totalOps.addAndGet (remainingOpsOnIterator);
+ //检查是否收到全部回复
+ if (xTotalOps == expectedTotalOps) {
+ onPhaseDone ();
+ } else if (xTota1Ops > expectedTotal0ps) {
+ throw new AssertionError(. ..);
+ }
+}
+
+
此处忽略了搜索结果totalHits为0的结果,并将结果进行累加,当xTotalOps等于expectedTotalOps时开始AbstractSearchAsyncAction.onPhaseDone再进行AbstractSearchAsyncAction.executeNextPhase取回阶段
取回阶段,图片来自官网,
(1)各个shard 返回的只是各文档的id和排序值 IDs and sort values ,coordinate node根据这些id&sort values 构建完priority queue之后,然后把程序需要的document 的id发送mget请求去所有shard上获取对应的document
(2)各个shard将document返回给coordinate node
(3)coordinate node将合并后的document结果返回给client客户端
Query阶段的executeNextPhase方法触发Fetch阶段,Fetch阶段的起点为FetchSearchPhase#innerRun函数,从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch来获取数据,其中包括分页信息。对scroll请求的处理也在FetchSearchPhase#innerRun函数中。
private void innerRun() throws Exception {
+ final int numShards = context.getNumShards();
+ final boolean isScrollSearch = context.getRequest().scroll() != null;
+ final List<SearchPhaseResult> phaseResults = queryResults.asList();
+ final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();
+ final boolean queryAndFetchOptimization = queryResults.length() == 1;
+ final Runnable finishPhase = () -> moveToNextPhase(
+ searchPhaseController,
+ queryResults,
+ reducedQueryPhase,
+ queryAndFetchOptimization ? queryResults : fetchResults.getAtomicArray()
+ );
+ for (int i = 0; i < docIdsToLoad.length; i++) {
+ IntArrayList entry = docIdsToLoad[i];
+ SearchPhaseResult queryResult = queryResults.get(i);
+ if (entry == null) {
+ if (queryResult != null) {
+ releaseIrrelevantSearchContext(queryResult.queryResult());
+ progressListener.notifyFetchResult(i);
+ }
+ counter.countDown();
+ }else{
+ executeFetch(
+ queryResult.getShardIndex(),
+ shardTarget,
+ counter,
+ fetchSearchRequest,
+ queryResult.queryResult(),
+ connection
+ );
+ }
+ }
+ }
+}
+
再看源码:
启动一个线程来fetch
+AbstractSearchAsyncAction.executePhase->FetchSearchPhase.run->FetchSearchPhase.innerRun->FetchSearchPhase.executeFetch
+
+private void executeFetch(...) {
+ context.getSearchTransport()
+ .sendExecuteFetch(
+ connection,
+ fetchSearchRequest,
+ context.getTask(),
+ new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {
+ @Override
+ public void innerOnResponse(FetchSearchResult result) {
+ progressListener.notifyFetchResult(shardIndex);
+ counter.onResult(result);
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ progressListener.notifyFetchFailure(shardIndex, shardTarget, e);
+ counter.onFailure(shardIndex, shardTarget, e);
+ }
+ }
+ );
+}
+
+
counter是一个收集器CountedCollector,onResult(result)主要是每次收到的shard数据存放,并且执行一次countDown,当所有shard数据收集完之后,然后触发一次finishPhase。
# CountedCollector.class
+void countDown() {
+ assert counter.isCountedDown() == false : "more operations executed than specified";
+ if (counter.countDown()) {
+ onFinish.run();
+ }
+}
+
moveToNextPhase方法执行下一阶段,下-阶段要执行的任务定义在FetchSearchPhase构造 函数中,主要是触发ExpandSearchPhase。
AbstractSearchAsyncAction.executePhase->ExpandSearchPhase.run。取回阶段完成之后执行ExpandSearchPhase#run,主要判断是否启用字段折叠,根据需要实现字段折叠功能,如果没有实现字段折叠,则直接返回给客户端。
ExpandSearchPhase执行完之后回复客户端,在AbstractSearchAsyncAction.sendSearchResponse方法中实现:
执行本流程的线程池: search。
对各种Query请求的处理入口注册于SearchTransportService.registerRequestHandler。
public static void registerRequestHandler(TransportService transportService, SearchService searchService) {
+ ...
+ transportService.registerRequestHandler(
+ QUERY_ACTION_NAME,
+ ThreadPool.Names.SAME,
+ ShardSearchRequest::new,
+ (request, channel, task) -> searchService.executeQueryPhase(
+ request,
+ (SearchShardTask) task,
+ new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request)
+ )
+ );
+ ...
+}
+
# SearchService
+public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) {
+ assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1
+ : "empty responses require more than one shard";
+ final IndexShard shard = getShard(request);
+ rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> {
+ ensureAfterSeqNoRefreshed(shard, orig, () -> executeQueryPhase(orig, task), l);
+ }));
+}
+
其中ensureAfterSeqNoRefreshed是把request任务放到一个名为search的线程池里面执行,容量大小为1000。
主要是用来执行SearchService.executeQueryPhase->SearchService.loadOrExecuteQueryPhase->QueryPhase.execute。核心的查询封装在queryPhase.execute(context)中,其中调用Lucene实现检索,同时实现聚合:
public void execute (SearchContext searchContext) {
+ aggregationPhase.preProcess (searchContext);
+ boolean rescore = execute ( searchContext, searchContext.searcher(), searcher::setCheckCancelled, indexSort);
+ if (rescore) {
+ rescorePhase.execute (searchContext);
+ suggestPhase.execute (searchContext);
+ aggregationPhase.execute (searchContext);
+ }
+}
+
其中包含几个核心功能:
对各种Fetch请求的处理入口注册于SearchTransportService.registerRequestHandler。
transportService.registerRequestHandler(
+ QUERY_FETCH_SCROLL_ACTION_NAME,
+ ThreadPool.Names.SAME,
+ InternalScrollSearchRequest::new,
+ (request, channel, task) -> {
+ searchService.executeFetchPhase(
+ request,
+ (SearchShardTask) task,
+ new ChannelActionListener<>(channel, QUERY_FETCH_SCROLL_ACTION_NAME, request)
+ );
+ }
+);
+
对Fetch响应的实现封装在SearchService.executeFetchPhase中,核心是调用fetchPhase.execute(context)。按照命中的doc取得相关数据,填充到SearchHits中,最终封装到FetchSearchResult中。
# FetchPhase
+public void execute(SearchContext context) {
+ Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().startProfilingFetchPhase();
+ SearchHits hits = null;
+ try {
+ //lucene构建搜索的结果
+ hits = buildSearchHits(context, profiler);
+ } finally {
+ ProfileResult profileResult = profiler.finish();
+ // Only set the shardResults if building search hits was successful
+ if (hits != null) {
+ context.fetchResult().shardResult(hits, profileResult);
+ }
+ }
+}
+
入口:RestCancellableNodeClient.doExecute Task task = client.executeLocally主要执行查询,并使用了ActionListener来进行监听
其中onResponse的调用链路如下:RestActionListener.onResponse->RestResponseListener.processResponse->RestController.sendResponse->DefaultRestChannel.sendResponse->Netty4HttpChannel.sendResponse
public void sendResponse(RestResponse restResponse) {
+ ...
+ httpChannel.sendResponse(httpResponse, listener);
+ ...
+}
+
+
最后由Netty4HttpChannel.sendResponse来响应请求。
当我们以aaaa*这样来搜索的时候,实际上查询了所有匹配以aaaa开头的索引,并且对所有的索引的分片都进行了一次Query,再然后对有结果的分片进行一次fetch,最终才能展示结果。可能觉得好奇,对所有分片都进行一次搜索,遍历分片所有的Lucene分段,会不会太过于消耗资源,因此合并Lucene分段对搜索性能有好处,这里下篇文章在讲吧。同时,聚合是发生在fetch过程中的,并不是lucene。
第一次听到执行计划还是挺懵的,感觉像是存储函数什么样的东西,后来发现,居然是explain
在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。
将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。
禁用原因:
1.命中率低
2.写时所有都失效
禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。
查询缓存讲解:https://blog.csdn.net/zzddada/article/details/124116182
DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。
公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。
主要还是基于jenkins里面构建一个自由风格的软件项目,当时参考的是阿里的codepipeline,就是对jenkins封装一层,包括创建job、立即构建、获取构建进度等都进行封装,并将需要的东西进行存库,没有想到码代码的时候,一堆的坑,比如: 1.连续点击立即构建,jenkins是不按顺序返回的,(分布式锁解决) 2.跨域调用,csrf,这个还好,不过容易把jenkins搞的无法登录(注意配置,具体可以点击这里) 3.创建job的时候只支持xml格式,还要转换一下,超级坑(xstream强行转换) 4.docker构建的时候,需要挂载宿主机的docker(想过用远程的,但效率不高) 5.数据库与jenkins的job一致性问题,任务创建失败,批量删除太慢(目前没想好怎么解决) 6.由于使用了数据库,需要检测job是否构建完成,为了自定义参数,我们自写了个通知插件,将构建状态返回到kafka,然后管理平台在进行消息处理。
完成了以上的东西,不过由于太过于简单,导致只能进行单条线的CICD,而且CI仅仅实现了打包,没有将CD的过程一同串行起来。简单来说就是,用户点击了构建只是能够打出一个镜像,但是如果要部署到kubernetes,还是需要在应用里手动更换一下镜像版本。总体而言,这个版本的jenkins我们使用的还是单点的,不足以支撑构建量比较大的情况,甚至如果当前服务挂了,断网了,整一块的构建功能都不能用。
<project>
+ <actions/>
+ <description>xxx</description>
+ <properties>
+ <hudson.model.ParametersDefinitionProperty>
+ <parameterDefinitions>
+ <hudson.model.TextParameterDefinition>
+ <name>buildParam</name>
+ <defaultValue>v1</defaultValue>
+ </hudson.model.TextParameterDefinition>
+ <hudson.model.TextParameterDefinition>
+ <name>codeBranch</name>
+ <defaultValue>master</defaultValue>
+ </hudson.model.TextParameterDefinition>
+ </parameterDefinitions>
+ </hudson.model.ParametersDefinitionProperty>
+ </properties>
+ <scm class="hudson.plugins.git.GitSCM">
+ <configVersion>2</configVersion>
+ <userRemoteConfigs>
+ <hudson.plugins.git.UserRemoteConfig>
+ <url>http://xxxxx.git</url>
+ <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>
+ </hudson.plugins.git.UserRemoteConfig>
+ </userRemoteConfigs>
+ <branches>
+ <hudson.plugins.git.BranchSpec>
+ <name>${codeBranch}</name>
+ </hudson.plugins.git.BranchSpec>
+ </branches>
+ <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+ <extensions/>
+ </scm>
+ <builders>
+ <hudson.tasks.Shell>
+ <command>ls</command>
+ </hudson.tasks.Shell>
+ <hudson.tasks.Maven>
+ <targets>clean package install -Dmaven.test.skip=true</targets>
+ <mavenName>mvn3.5.4</mavenName>
+ </hudson.tasks.Maven>
+ <com.cloudbees.dockerpublish.DockerBuilder>
+ <server>
+ <uri>unix:///var/run/docker.sock</uri>
+ </server>
+ <registry>
+ <url>http://xxxx</url>
+ </registry>
+ <repoName>xxx/xx</repoName>
+ <forcePull>true</forcePull>
+ <dockerfilePath>Dockerfile</dockerfilePath>
+ <repoTag>${buildParam}</repoTag>
+ <skipTagLatest>true</skipTagLatest>
+ </com.cloudbees.dockerpublish.DockerBuilder>
+ </builders>
+ <publishers>
+ <com.xxxx.notifications.Notifier/>
+ </publishers>
+</project>
+
上面的过程也仍然没有没住DevOps的流程,人工干预的东西依旧很多,由于上级急需产出,所以只能将就着继续下去。我们将构建、部署每个当做一个小块,一个CICD的过程可以选择构建、部署,花了很大的精力,完成了串行化的别样的CICD。 以下图为例,整个流程的底层为:paas平台-jenkins-kakfa-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发构建-jenkins-kafka-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发部署。
目前实现了串行化的CICD构建部署,之后考虑实现多个CICD并行,并且一个CICD能够调用另一个CICD,实际运行中,出现了一大堆问题。由于经过的组件太多,一次cicd的运行报错,却很难排查到问题出现的原因,业务方的投诉也开始慢慢多了起来,只能说劝导他们不要用这个功能。
没有CICD,就无法帮助公司上容器云,无法合理的利用容器云的特性,更无法走上云原生的道路。于是,我们决定另谋出路。
由于之前的CICD问题太多,特别是经过的组件太多了,导致出现问题的时候无法正常排查,为了能够更加稳定可靠,还是决定了要更换一下底层。 我们重新审视了下pipeline,觉得这才是正确的做法,可惜不知道如果做成一个产品样子的东西,用户方Dockerfile都不怎么会写,你让他写一个Jenkinsfile?不合理!在此之外,我们看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自带了cicd的工具,需要配置一下runner,然后配置一下.gitlab-ci.yml写一下程序的cicd过程即可,构建镜像的时候我们使用的是kaniko,整个gitlab的cicd在我们公司小项目中大范围使用,但是学习成本过高,尤其是引入了kaniko之后,还是寻找一个产品化的CICD方案。
分布式构建jenkins x 首先要解决的是多个构建同时运行的问题,很久之前就调研过jenkins x,它必须要使用在kubernetes上,由于当时官方文档不全,而且我们的DevOps项目处于初始期,所有没有使用。jenkins的master slave结构就不多说了。jenkins x应该说是个全家桶,包含了helm仓库、nexus仓库、docker registry等,代码是jenkins-x-image。
serverless jenkins 好像跟谷歌的tekton相关,用了下,没调通,只能用于GitHub。感觉还不如直接使用tekton。
阿里云云效 提供了图形化配置DevOps流程,支持定时触发,可惜没有跟gitlab触发结合,如果需要个公司级的DevOps,需要将公司的jira、gitlab、jenkins等集合起来,但是图形化jenkins pipeline是个特别好的参考方向,可以结合阿里云云效来做一个自己的DevOps产品。
微软Pipeline 微软也是提供了DevOps解决方案的,也是提供了yaml格式的写法,即:在右边填写完之后会转化成yaml。如果想把DevOps打造成一款产品,这样的设计显然不是最好的。
谷歌tekton kubernetes的官方cicd,目前已用于kubernetes的release发版过程,目前也仅仅是与GitHub相结合,gitlab无法使用,全过程可使用yaml文件来创建,跑起来就是类似kubernetes的job一样,用完即销毁,可惜目前比较新,依旧处于alpha版本,无法用于生产。有兴趣可以参考下:Knative 初体验:CICD 极速入门
在调研DockOne以及各个产商的DevOps产品时,发现,真的只有阿里云的云效才是真正比较完美的DevOps产品,用户不需要知道pipeline的语法,也不需要掌握kubernetes的相关知识,甚至不用写yaml文件,对于开发、测试来说简直就是神一样的存在了。云效对小公司(创业公司)免费,但是有一定的量之后,就要开始收费了。在调研了一番云效的东西之后,发现云效也是基于jenkins x改造的,不过阿里毕竟人多,虽然能约莫看出是pipeline的语法,但是阿里彻底改造成了能够使用yaml来与后台交互。 下面是以阿里云的云效界面以及配合jenkins的pipeline语法来讲解:
PMD是一款可拓展的静态代码分析器它不仅可以对代码分析器,它不仅可以对代码风格进行检查,还可以检查设计、多线程、性能等方面的问题。阿里云的是简单的集成了一下而已,对于我们来说,底层使用了sonar来接入,所有的代码扫描结果都接入了sonar。
stage('Clone') {
+ steps{
+ git branch: 'master', credentialsId: 'xxxx', url: "xxx"
+ }
+}
+stage('check') {
+ steps{
+ container('maven') {
+ echo "mvn pmd:pmd"
+ }
+ }
+}
+
Java的单元测试一般用的是Junit,在阿里云中,使用了surefire插件,用来在maven构建生命周期的test phase执行一个应用的单元测试。它会产生两种不同形式的测试结果报告。我这里就简单的过一下,使用"mvn test"命令来代替。
+stage('Clone') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+ }
+}
+stage('test') {
+ steps{
+ container('maven') {
+ sh "mvn test"
+ }
+ }
+}
+
镜像的构建比较想使用kaniko,尝试找了不少方法,到最后还是只能使用dind(docker in docker),挂载宿主机的docker来进行构建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,挂载的时候需要配置一下config.json,然后挂载到容器的/root/.docker目录,才能在容器中使用docker。
为什么不推荐dind:挂载了宿主机的docker,就可以使用docker ps查看正在运行的容器,也就意味着可以使用docker stop、docker rm来控制宿主机的容器,虽然kubernetes会重新调度起来,但是这一段的重启时间极大的影响业务。
+stage('下载代码') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+ script {
+ build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+ }
+ }
+}
+stage('打包并构建镜像') {
+ steps{
+ container('maven') {
+ echo "3.Build Docker Image Stage"
+ sh "mvn clean install -Dmaven.test.skip=true"
+ sh "docker build -f xxx/Dockerfile -t xxxxxx:${build_tag} ."
+ sh "docker push xxxxxx:${build_tag}"
+ }
+ }
+}
+
+
CD过程有点困难,由于我们的kubernetes平台是图形化的,类似于阿里云,用户根本不需要自己写deployment,只需要在图形化界面做一下操作即可部署。对于CD过程来说,如果应用存在的话,就可以直接替换掉镜像版本即可,如果没有应用,就提供个简单的界面让用户新建应用。当然,在容器最初推行的时候,对于用户来说,一下子需要接受docker、kubernetes、helm等概念是十分困难的,不能一个一个帮他们写deployment这些yaml文件,只能用helm创建一个通用的spring boot或者其他的模板,然后让业务方修改自己的配置,每次构建的时候只需要替换镜像即可。
def tmp = sh (
+ returnStdout: true,
+ script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \$1}'"
+)
+//如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+if(tmp.equals('')){
+ sh "helm init --client-only"
+ sh """helm repo add mychartmuseum http://xxxxxx \
+ --username myuser \
+ --password=mypass"""
+ sh """helm install --set name=${JOB_NAME} \
+ --set namespace=${namespace} \
+ --set deployment.image=${image} \
+ --set deployment.imagePullSecrets=${harborProject} \
+ --name ${namespace}-${JOB_NAME} \
+ mychartmuseum/soa-template"""
+}else{
+ println "已经存在,替换镜像"
+ //epaas中一个pod的容器名称需要带上"-0"来区分
+ sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"
+}
+
+
代码扫描,单元测试,构建镜像三个并行运行,等三个完成之后,在进行部署
pipeline:
pipeline {
+ agent {
+ label "jenkins-maven"
+ }
+ stages{
+ stage('代码扫描,单元测试,镜像构建'){
+ parallel {
+ stage('并行任务一') {
+ agent {
+ label "jenkins-maven"
+ }
+ stages('Java代码扫描') {
+ stage('Clone') {
+ steps{
+ git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+ }
+ }
+ stage('check') {
+ steps{
+ container('maven') {
+ echo "$BUILD_NUMBER"
+ }
+ }
+ }
+ }
+ }
+ stage('并行任务二') {
+ agent {
+ label "jenkins-maven"
+ }
+ stages('Java单元测试') {
+ stage('Clone') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+ }
+ }
+ stage('test') {
+ steps{
+ container('maven') {
+ echo "3.Build Docker Image Stage"
+ sh "mvn -v"
+ }
+ }
+ }
+ }
+ }
+ stage('并行任务三') {
+ agent {
+ label "jenkins-maven"
+ }
+ stages('java构建镜像') {
+ stage('Clone') {
+ steps{
+ echo "1.Clone Stage"
+ git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+ script {
+ build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+ }
+ }
+ }
+ stage('Build') {
+ steps{
+ container('maven') {
+ echo "3.Build Docker Image Stage"
+ sh "mvn clean install -Dmaven.test.skip=true"
+ sh "docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:${build_tag} ."
+ sh "docker push hub.gcloud.lab/rongqiyun/epaas:${build_tag}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ stage('部署'){
+ stages('部署到容器云') {
+ stage('check') {
+ steps{
+ container('maven') {
+ script{
+ if (deploy_app == "true"){
+ def tmp = sh (
+ returnStdout: true,
+ script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \$1}'"
+ )
+ //如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+ if(tmp.equals('')){
+ sh "helm init --client-only"
+ sh """helm repo add mychartmuseum http://xxxxxx \
+ --username myuser \
+ --password=mypass"""
+ sh """helm install --set name=${JOB_NAME} \
+ --set namespace=${namespace} \
+ --set deployment.image=${image} \
+ --set deployment.imagePullSecrets=${harborProject} \
+ --name ${namespace}-${JOB_NAME} \
+ mychartmuseum/soa-template"""
+ }else{
+ println "已经存在,替换镜像"
+ //epaas中一个pod的容器名称需要带上"-0"来区分
+ sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"
+ }
+ }else{
+ println "用户选择不部署代码"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
在jenkins x中查看:
jenkins blue ocean步骤日志:
云效中的日志:
triggers {
+ cron('H H * * *') //每天
+ }
+
pipeline中除了有对于时间的trigger,还支持了gitlab的触发,需要各种配置,不过如果真的对于gitlab的cicd有要求,直接使用gitlab-ci会更好,我们同时也对gitlab进行了runner的配置来支持gitlab的cicd。gitlab的cicd也提供了构建完后即销毁的过程。
功能最强大的过程莫过于自己使用pipeline脚本实现,选取最适合自己的,但是对于一个公司来说,如果要求业务方来掌握这些,特别是IT流动性大的时候,既需要重新培训,同个问题又会被问多遍,所以,只能将DevOps实现成一个图形化的东西,方便,简单,相对来说功能还算强大。
DevOps最难的可能都不是以上这些,关键是让用户接受,容器云最初推行时,公司原本传统的很多发版方式都需要进行改变,有些业务方不愿意改,或者有些代码把持久化的东西存到了代码中而不是分布式存储里,甚至有些用户方都不愿意维护老代码,看都不想看然后想上容器,一个公司在做技术架构的时候,过于混乱到最后填坑要么需要耗费太多精力甚至大换血。
最后,DevOps是云原生的必经之路!!!
文章同步:
博客园:https://www.cnblogs.com/w1570631036/p/11524673.html
个人网站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
gitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai
tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决
tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,tlab相关 7.happens-before介绍一下 8.总线风暴是什么,怎么解决 9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决 10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222 11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作
tt三面: 大部分聊项目,基础随便问了问 1.分布式事务,两阶段三阶段流程,区别 2.mysql主从,同步复制,半同步复制,异步复制 3.算法题,k个一组翻转链表,类似lc25,区别是最后一组不足k个也要翻转 4.场景题,设计一个评论系统,包含发布评论,回复评论,评论点赞等功能,用户过亿,qps百万
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
+
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
+
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
+
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
+
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
+
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
+
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。 典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
+
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
+
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些: -XX:+PrintGC 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用 输出形式:Application time: 0.5291524 seconds -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用 输出形式:Total time for which application threads were stopped: 0.0468229 seconds -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息 输出形式:
34.702: [GC {Heap before gc invocations=7:
+ def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
+from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)
+ to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
+ tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
+ compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+ the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+ ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+ rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
+ def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
+ from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
+ to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)
+ tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
+ compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+ the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+ ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+ rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+}
+, 0.0757599 secs]
+
-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。 常见配置汇总
-Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:设置年轻代大小 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -XX:MaxPermSize=n:设置持久代大小
-XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息 持久代并发收集次数 传统GC信息 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间,一般会提高应用的效率 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他 会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么 并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置: -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。 -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG
使用top查看:
启动iptables,参考http://www.setphp.com/981.htmlhttp://www.setphp.com/981.html
iptables -A INPUT -s xmr.crypto-pool.fr -j DROP
+iptables -A OUTPUT -d xmr.crypto-pool.fr -j DROP
+
19号添加mongodb之后,20号重启了服务器,但是忘记启动mongodb,导致后台一直在重连mongodb,也就导致了服务访问超级超级慢,记住要启动所需要的组件。
前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 该程序task主要分为三个模块: console进行一些cron的配置(表达式、任务名称、任务组等); schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发; client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。 整体架构跟github上开源的xxl-job类似,也可以参考一下。
容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n "
+JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
+
其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。
堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。
先从容器中dump出堆内存
jmap -dump:live,format=b,file=heap.hprof 58
+
由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。
另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。 首先是查看堆内存的中占比:
可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:
达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。
声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。
最终解决办法: 在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully
提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了
还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。
第一个想到的就是netty的直接内存,关掉,命令如下:
-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced
+
查看了一下java的nio直接内存,发现也就几十kb,然而直接内存还是慢慢往上涨。毫无头绪,然后开始了自己的从linux层面开始排查问题
一般配合pmap使用,从内核中读取内存块,然后使用views 内存块来判断错误,我简单试了下,乱码,都是二进制的东西,看不出所以然来。
pmap -d 58 | sort -n -k2
+pmap -x 58 | sort -n -k3
+grep rw-p /proc/$1/maps | sed -n 's/^\([0-9a-f]*\)-\([0-9a-f]*\) .*$/\1 \2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done
+
这个时候真的懵了,不知道从何入手了,难道是linux操作系统方面的问题?
起初,在网上看到有人说是因为linux自带的glibc版本太低了,导致的内存溢出,考虑一下。初步觉得也可能是因为这个问题,所以开始慢慢排查。oracle官方有一个jemalloc用来替换linux自带的,谷歌那边也有一个tcmalloc,据说性能比glibc、jemalloc都强,开始换一下。 根据网上说的,在容器里装libunwind,然后再装perf-tools,然后各种捣鼓,到最后发现,执行不了,
pprof --text /usr/bin/java java_58.0001.heap
+
看着工具高大上的,似乎能找出linux的调用栈,
毫无头绪的时候,回想到了linux的top命令以及日志情况,测试环境是由于太多执行端业务方都没有维护,导致调度系统一直会出错,一出错就会导致大量刷错误日志,平均一天一个容器大概就有3G的日志,cron一旦到准点,就会有大量的任务要同时执行,而且容器中是做了对io的限制,磁盘也限制为10G,导致大量的日志都堆积在buff/cache里面,最终直接内存一直在涨,这个时候,系统不会挂,但是先会一直显示内存使用率达到100%。 修复后的结果如下图所示:
定时调度这个系统当时并没有考虑到公司的系统会用的这么多,设计的时候也仅仅是为了实现上千的量,没想到到最后变成了一天的调度都有几百万次。最初那批开发也就使用了大量的本地缓存map来临时存储数据,然后面向简历编程各种用netty自己实现了通信的方式,一堆坑都留给了后人。目前也算是解决掉了一个由于线程过多导致系统不可用的情况而已,但是由于存在大量的map,系统还是得偶尔重启一下比较好。
参考:
1.记一次线上内存泄漏问题的排查过程
2.Java堆外内存增长问题排查Case
3.Troubleshooting Native Memory Leaks in Java Applications
四个内存屏障指令:LoadLoad,
StoreStore,
LoadStore,
StoreLoad
在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值
在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。
在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。
虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。
首先,我们将看到“平台线程”和“虚拟线程”之间的主要区别。接下来,我们将使用虚拟线程从头开始构建一个 Spring-Boot 应用程序。最后,我们将创建一个小型测试套件,以查看简单 Web 应用程序吞吐量的最终改进。
主要区别在于虚拟线程在其操作周期中不依赖于操作系统线程:它们与硬件解耦,因此有了“虚拟”这个词。这种解耦是由 JVM 提供的抽象层实现的。
对于本教程来说,必须了解虚拟线程的运行成本远低于平台线程。它们消耗的分配内存量要少得多。这就是为什么可以创建数百万个虚拟线程而不会出现内存不足问题,而不是使用标准平台(或内核)线程创建几百个虚拟线程。
从理论上讲,这赋予了开发人员一种超能力:无需依赖异步代码即可管理高度可扩展的应用程序。
从 Spring Framework 6(和 Spring Boot 3)开始,虚拟线程功能正式公开,但虚拟线程是Java 19 的预览功能。这意味着我们需要告诉 JVM 我们要在应用程序中启用它们。由于我们使用 Maven 来构建应用程序,因此我们希望确保在 pom.xml 中包含以下代码:
<build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>19</source>
+ <target>19</target>
+ <compilerArgs>
+ --enable-preview
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ </plugins>
+</build>
+
从 Java 的角度来看,要使用 Apache Tomcat 和虚拟线程,我们需要一个带有几个 bean 的简单配置类:
@EnableAsync
+@Configuration
+@ConditionalOnProperty(
+ value = "spring.thread-executor",
+ havingValue = "virtual"
+)
+public class ThreadConfig {
+ @Bean
+ public AsyncTaskExecutor applicationTaskExecutor() {
+ return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
+ }
+
+ @Bean
+ public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
+ return protocolHandler -> {
+ protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+ };
+ }
+}
+
第一个 Spring Bean ApplicationTaskExecutor将取代标准的*ApplicationTaskExecutor* ,提供为每个任务启动新虚拟线程的Executor。第二个 bean,名为ProtocolHandlerVirtualThreadExecutorCustomizer,将以相同的方式 自定义标准TomcatProtocolHandler 。我们还添加了注释@ConditionalOnProperty,**以通过切换application.yaml文件中配置属性的值来按需启用虚拟线程:
spring:
+ thread-executor: virtual
+ //...
+
我们来测试一下Spring Boot应用程序是否使用虚拟线程来处理Web请求调用。为此,我们需要构建一个简单的控制器来返回所需的信息:
@RestController
+@RequestMapping("/thread")
+public class ThreadController {
+ @GetMapping("/name")
+ public String getThreadName() {
+ return Thread.currentThread().toString();
+ }
+}
+
*Thread*对象的toString *()*方法将返回我们需要的所有信息:线程 ID、线程名称、线程组和优先级。让我们通过一个curl请求来访问这个端点:
$ curl -s http://localhost:8080/thread/name
+$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4
+
正如我们所看到的,响应明确表示我们正在使用虚拟线程来处理此 Web 请求。换句话说,*Thread.currentThread()*调用返回虚拟线程类的实例。现在让我们通过简单但有效的负载测试来看看虚拟线程的有效性。
对于此负载测试,我们将使用JMeter。这不是虚拟线程和标准线程之间的完整性能比较,而是我们可以使用不同参数构建其他测试的起点。
在这种特殊的场景中,我们将调用Rest Controller中的一个端点,该端点将简单地让执行休眠一秒钟,模拟复杂的异步任务:
@RestController
+@RequestMapping("/load")
+public class LoadTestController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);
+
+ @GetMapping
+ public void doSomething() throws InterruptedException {
+ LOG.info("hey, I'm doing something");
+ Thread.sleep(1000);
+ }
+}
+
请记住,由于*@ConditionalOnProperty* 注释,我们只需更改 application.yaml 中变量的值即可在虚拟线程和标准线程之间切换。
JMeter 测试将仅包含一个线程组,模拟 1000 个并发用户访问*/load* 端点 100 秒:
在本例中,采用这一新功能所带来的性能提升是显而易见的。让我们比较不同实现的“响应时间图”。这是标准线程的响应图。我们可以看到,立即完成一次调用所需的时间达到 5000 毫秒:
发生这种情况是因为平台线程是一种有限的资源,当所有计划的和池化的线程都忙时,Spring 应用程序除了将请求搁置直到一个线程空闲之外别无选择。
让我们看看虚拟线程会发生什么:
正如我们所看到的,响应稳定在 1000 毫秒。虚拟线程在请求后立即创建和使用,因为从资源的角度来看它们非常便宜。在本例中,我们正在比较 spring 默认固定标准线程池(默认为 200)和 spring 默认无界虚拟线程池的使用情况。
**这种性能提升之所以可能,是因为场景过于简单,并且没有考虑 Spring Boot 应用程序可以执行的全部操作。**从底层操作系统基础设施中采用这种抽象可能是有好处的,但并非在所有情况下都是如此。
做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。
限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。
限流可以应对:
对于限流场景,一般需要考虑两个维度的信息: 时间 限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定 资源 基于可用资源的限制,比如设定最大访问次数,或最高可用连接数。 限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。
分布式限流相比于单机限流,只是把限流频次分配到各个节点中,比如限制某个服务访问100qps,如果有10个节点,那么每个节点理论上能够平均被访问10次,如果超过了则进行频率限制。
基于Guava的客户端限流 Guava是一个客户端组件,在其多线程模块下提供了以RateLimiter为首的几个限流支持类。它只能对“当前”服务进行限流,即它不属于分布式限流的解决方案。
网关层限流 服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求。我们在网关层进行限流,就可以达到了整体限流的目的了。目前,主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件为代表的F5。
中间件限流 将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。
限流组件 目前也有一些开源组件提供了限流的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中。Hystrix也具有限流的功能。
Guava的Ratelimiter设计实现相当不错,可惜只能支持单机,网关层限流如果是单机则不太满足高可用,并且分布式网关的话还是需要依赖中间件限流,而redis之类的网络通信需要占用一小部分的网络消耗。阿里的Sentinel也是同理,底层使用的是redis或者zookeeper,每次访问都需要调用一次redis或者zk的接口。那么在云原生场景下,我们有没有什么更好的办法呢?
对于极致追求高性能的服务不需要考虑熔断、降级来说,是需要尽量减少网络之间的IO,那么是否可以通过一个总限频然后分配到具体的单机里面去,在单机中实现平均的限流,比如限制某个ip的qps为100,服务总共有10个节点,那么平均到每个服务里就是10qps,此时就可以通过guava的ratelimiter来实现了,甚至说如果服务的节点动态调整,单个服务的qps也能动态调整。
在Spring Boot应用中,定义一个filter,获取请求参数里的key(ip、userId等),然后根据key来获取rateLimiter,其中,rateLimiter的创建由数据库定义的限频数和副本数来判断,最后,再通过rateLimiter.tryAcquire来判断是否可以通过。
在实际的服务中,数据上报服务一般无法确定客户端的上报时间、上报量,特别是对于这种要求高性能,服务一般都会用到HPA来实现动态扩缩容,所以,需要去间隔一段时间去获取服务的副本数。
func CountDeploymentSize(namespace string, deploymentName string) *int32 {
+ deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
+ if err != nil {
+ return nil
+ }
+ return deployment.Spec.Replicas
+}
+
用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。
在RateLimiterService中定义一个LoadingCache<String, RateLimiter>,其中,key可以为ip、userId等,并且,在多线程的情况下,使用refreshAfterWrite只阻塞加载数据的线程,其他线程则返回旧数据,极致发挥缓存的作用。
private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()
+ .maximumSize(10_000)
+ .refreshAfterWrite(20, TimeUnit.MINUTES)
+ .build(this::createRateLimit);
+//定义一个默认最小的QPS
+private static final Integer minQpsLimit = 3000;
+
之后是创建rateLimiter,获取总限频数totalLimit和副本数replicas,之后是自己所需的逻辑判断,可以根据totalLimit和replicas的情况来进行qps的限定。
public RateLimiter createRateLimit(String key) {
+ log.info("createRateLimit,key:{}", key);
+ int totalLimit = 获取总限频数,可以在数据库中定义
+ Integer replicas = kubernetesService.getDeploymentReplicas();
+ RateLimiter rateLimiter;
+ if (totalLimit > 0 && replicas == null) {
+ rateLimiter = RateLimiter.create(totalLimit);
+ } else if (totalLimit > 0) {
+ int nodeQpsLimit = totalLimit / replicas;
+ rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);
+ } else {
+ rateLimiter = RateLimiter.create(minQpsLimit);
+ }
+ log.info("create rateLimiter success,key:{},rateLimiter:{}", key, rateLimiter);
+ return rateLimiter;
+}
+
根据key获取RateLimiter,如果有特殊需求的话,需要判断key不存在的尝尽
public RateLimiter getRateLimiter(String key) {
+ return loadingCache.get(key);
+}
+
最后一步,就是使用rateLimiter来进行限流,如果rateLimiter.tryAcquire()为true,则进行filterChain.doFilter(request, response),如果为false,则返回HttpStatus.TOO_MANY_REQUESTS
public class RateLimiterFilter implements Filter {
+ @Resource
+ private RateLimiterService rateLimiterService;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+ HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+ String key = httpServletRequest.getHeader("key");
+ RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);
+ if (rateLimiter != null) {
+ if (rateLimiter.tryAcquire()) {
+ filterChain.doFilter(request, response);
+ } else {
+ httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
+ }
+ } else {
+ filterChain.doFilter(request, response);
+ }
+ }
+}
+
为了方便对比性能之间的差距,我们在本地单机做了下列测试,其中,总限频都设置为3万。
无限流
使用redis限流
其中,ping redis大概6-7ms左右,对应的,每次请求需要访问redis,时延都有大概6-7ms,性能下降明显
自研限流
性能几乎追平无限流的场景,guava的rateLimiter确实表现卓越
5.1 对于保证qps限频准确的时候,应该怎么解决呢?
在k8s中,服务是动态扩缩容的,相应的,每个节点应该都要有所变化,如果对外宣称限频100qps,而且后续业务方真的要求百分百准确,只能把LoadingCache<String, RateLimiter>的过期时间调小一点,让它能够近实时的更新单节点的qps。这里还需要考虑一下k8s的压力,因为每次都要获取副本数,这里也是需要做缓存的
5.2 服务从1个节点动态扩为4个节点,这个时候新节点识别为4,但其实有些并没有启动完,会不会造成某个节点承受了太大的压力
理论上是存在这个可能的,这个时候需要考虑一下初始的副本数的,扩缩容不能一蹴而就,一下子从1变为4变为几十个这种。一般的话,生产环境肯定是不能只有一个节点,并且要考虑扩缩容的话,至于要有多个副本预备的
5.3 如果有多个副本,怎么保证请求是均匀的
这个是依赖于k8s的service负载均衡策略的,这个我们之前做过实验,流量确实是能够均匀的落到节点上的。还有就是,我们整个限流都是基于k8s的,如果k8s出现问题,那就是整个集群所有服务都有可能出现问题了。
1.常见的分布式限流解决方案
2.分布式服务限流实战
3.高性能
Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。
使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。
Spark Operator包括如下几个组件:
安装
# 添加源
+helm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operator
+# 安装
+helm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace
+
这个时候如果直接执行kubectl apply -f examples/spark-pi.yaml调用样例,会出现以下报错,需要创建对象的serviceaccount。
forbidden: error looking up service account default/spark: serviceaccount \"spark\" not found.\n\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:568)\n\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport
+
同时也要安装rbac
rbac的文件在manifest/spark-operator-install/spark-operator-rbac.yaml,需要把namespace全部改为需要运行的namespace,这里我们任务放在default命名空间下,所以全部改为default,源文件见rbac.yaml
执行脚本,这里用的app.jar是一个读取es数据源,然后计算,为了展示效果,在程序的后面用了TimeUnit.HOURS.sleep(1)方便观察。
代码如下:
SparkConf sparkConf = new SparkConf().setAppName("Spark WordCount Application (java)");
+ sparkConf.set("es.nodes", "xxx")
+ .set("es.port", "8xx0")
+ .set("es.nodes.wan.only", "true")
+ .set("es.net.http.auth.user", "xxx")
+ .set("es.net.http.auth.pass", "xxx-xxx");
+ try (JavaSparkContext jsc = new JavaSparkContext(sparkConf)) {
+ JavaRDD<Map<String, Object>> esRDD = JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce").values();
+
+ System.out.println(esRDD.partitions().size());
+
+ esRDD.map(x -> x.get("customer_full_name"))
+ .countByValue()
+ .forEach((x, y) -> System.out.println(x + ":" + y));
+ TimeUnit.HOURS.sleep(1);
+
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
打包之后kubectl create -f k8s.yaml即可
apiVersion: "sparkoperator.k8s.io/v1beta2"
+kind: SparkApplication
+metadata:
+ name: spark-demo
+ namespace: default
+spec:
+ type: Scala
+ mode: cluster
+ image: fewfew-docker.pkg.coding.net/spark-java-demo/spark-java-demo-new/spark-java-demo:master-1d8c164bced70a1c66837ea5c0180c61dfb48ac3
+ imagePullPolicy: Always
+ mainClass: com.spark.es.demo.EsReadGroupByName
+ mainApplicationFile: "local:///opt/spark/examples/jars/app.jar"
+ sparkVersion: "3.1.1"
+ restartPolicy:
+ type: Never
+ driver:
+ # 用cores必须要大于等于1,这里用coreRequest
+ coreRequest: "100m"
+ coreLimit: "1200m"
+ memory: "512m"
+ labels:
+ version: 3.1.1
+ serviceAccount: sparkoperator
+ executor:
+ # 用cores必须要大于等于1,这里用coreRequest
+ coreRequest: "100m"
+ instances: 1
+ memory: "512m"
+ labels:
+ version: 3.1.1
+
spark-demo-driver 1/1 Running 0 2m30s
+spark-wordcount-application-java-0ac352810a9728e1-exec-1 1/1 Running 0 2m1s
+
同时会自动生成相应的service。
Spark on k8s operator大大减少了spark的部署与运维成本,用容器的调度来替换掉yarn,
Spark Operator的主要组件如下:
1、SparkApplication Controller : 用于监控并相应SparkApplication的相关对象的创建、更新和删除事件;
2、Submission Runner:用于接收Controller的提交指令,并通过spark-submit 来提交Spark作业到K8S集群并创建Driver Pod,driver正常运行之后将启动Executor Pod;
3、Spark Pod Monitor:实时监控Spark作业相关Pod(Driver、Executor)的运行状态,并将这些状态信息同步到Controller ;
4、Mutating Admission Webhook:可选模块,但是在Spark Operator中基本上所有的有关Spark pod在Kubernetes上的定制化功能都需要使用到该模块,因此建议将enableWebhook这个选项设置为true。
5、sparkctl: 基于Spark Operator的情况下可以不再使用kubectl来管理Spark作业了,而是采用Spark Operator专用的作业管理工具sparkctl,该工具功能较kubectl功能更为强大、方便易用。
apis:用户编写yaml时的解析
Batchscheduler:批处理的调度,提供了支持volcano的接口
Client:
leader的选举
electionCfg := leaderelection.LeaderElectionConfig{
+ Lock: resourceLock,
+ LeaseDuration: *leaderElectionLeaseDuration,
+ RenewDeadline: *leaderElectionRenewDeadline,
+ RetryPeriod: *leaderElectionRetryPeriod,
+ Callbacks: leaderelection.LeaderCallbacks{
+ OnStartedLeading: func(c context.Context) {
+ close(startCh)
+ },
+ OnStoppedLeading: func() {
+ close(stopCh)
+ },
+ },
+}
+
+elector, err := leaderelection.NewLeaderElector(electionCfg)
+
参考:
2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?
回到学校,待了几天,那个整天给别人灌毒鸡汤的人还是不着急找工作:该有的时候总会有的啦,然后继续打游戏、那个天天喊着吃麻辣香锅的人,记忆中好像今年都没有吃上,哈哈、厕所还是那样偶尔炸一下,嗯,感觉一切都很熟悉,只是,我有了点伤感,毕竟身边又要换一群不认识的人了。考完试,终于能回广东了,一年没回了,开心不得了,更多的是,为了那个喜欢了好几年没有表白的妹纸,之前从没想过真的能每天跟一个妹纸道晚安,秒回,但是,对她,我还真做到了。1月份的广州,不热不冷,空气很清新,比北京、天津那股泥土味好多了,只是,我被拒了,男闺蜜还是男闺蜜,备胎还是备胎,那会还没想过放弃,直到...
春节期间,把自己的网站整理了一下,看了看JVM和并发的知识,偶尔刷刷牛客网的题,觉得自己没多大问题了,跟同学出发,去深圳,我并没有一个做马化腾、马云的梦,但是我想,要做出点东西,至少,不是个平庸的人。投了几个,第一个面试,知道是培训兼外包后果断弃了,春招还没开始,再多准备准备,有时间就往深圳图书馆里跑,找个位置,静静的复习,那会,感觉自己一定能去个大公司吧,搞搞分布式、大数据之类的~
2月份底开始慢慢投简历,跑招聘会,深圳招聘的太少了,那就去广州,大学城,于是,开始了自己一周两天都在广深往返。有的公司笔试要跑一次广州,一面要跑一次,二面还要跑一次,每次看着路费,真的承受不起,但是有希望就行。乐观之下,到来的是一重又一重的打击。
**数码,很看好的公司,在广州也算是老品牌了,笔试的时候面试官没有带够卷子,就拿了大数据的做,还过了,面试的时候被面到union和union all的区别,答反了,还有left join和right join的原理,一脸懵逼,过后去了百度搜了搜,这东西哪来的原理?
然后是个企业建站的,笔试完了,面试官照着题问然后没说啥就让我走了,额,我好像还没自我介绍呢。
有点想回北京了,往北京投了一份,A轮公司,月薪10k,过了,觉得自己技术应该不差吧,不过还是拒了,因为一想起那里满脑子都是雾霾。
没事没事,下一家,某游戏公司,笔试过了,一面也过了,然后被问flume收集的时候断网处理机制、MD5和RSA原理、Angulajs和Vuejs用没用过、千万级的数据是如何存储的?额,跟你们想的一样,我一道也答出来,然后就挂了,面试官跟我说,你要是来早三个月这些都会啦,说的我很紧张,广东的同学都那么强么?
这个时候还没质疑自己的技术,去了趟深圳某家小公司,金融的,HR问我是不是培训过来的,心里开始突然间好苦涩,大学花那么多时间学习居然被别人用来和培训机构的人相提并论,笔试不知从哪整来的英文逻辑题,做完了然后上机笔试,做不出来不能见面试官,开始感觉这公司有点。。。。还是撤了撤了。
突然感觉面试面的绝望,是自己的问题么?是自己技术太菜了么?开始怀疑自己,感觉也是,已经3月份底了,毕竟别人都在实习,而我已经离职3个月没工作了,但是那些问题不是有点离谱了,一开始是开始心态爆炸觉得怎么可以对应届生问这些问题,不应该是基础么?算法什么的,后来是无奈了,开始上网搜各种不沾边的技术。
还有其他的面试。。。有的工资不高(6K不到),有的面试问题全答对就因为说了有点想做大数据就不要你,有的聊得很欢快却没消息,有的说招java但是笔试一道java题都没有全是前端的,有的打着互联网实际却是外包的公司。。。。 4月初真的很绝望很绝望,不想跟别人说话,是不是很可笑,一个从北京B+轮的公司回广东,面试近15个,居然只能拿到2个offer,其中之一国企外包的,都不知说什么好。13号要拍毕业照了,没有回去,大学四年,毕竟大家个个圈子之间没什么交流,没什么友谊,果然,整个专业的人只有2/3的人去拍了毕业照。4月23号,拉勾网深圳站,梦想着市集,抱着一点点的小希望,去投了5个,结果一个面试邀请都没有,换之而来的是一片绝望、黑暗。。。。
5月初回学校了赶毕业设计,回学校也是,天天往图书馆跑,为了面试,毕业设计拉下太多了。悠闲的时候,总会想起我喜欢的那个人,过的怎么样了,微信一开,好像自己被屏蔽了还是被删了?这三个月来的面试打击,真的不知道怎么应对,我更不知道怎么跟她聊天,以至于慢慢隔离,这段感情,或许只有我知道自己有多心疼,只有我自己sb得一点点的呵护着。想起上次跟她聊天是4月中,还是她提了个问题其他什么都没问,彼此之前已经没有关心了。删了吧删了吧,就这样一句再见都没有说,一句“祝福”都没有说。 5月底遇到两次还算开心的事,导师问我要不要评优秀论文,不是很想折腾,拒了,导师仍旧劝我,只是写个3页的小总结而已,评上的话能留图书馆收藏。另一个件是珍爱网打电话过来说我一面过了,什么时候二面,天啊,3月份一面,5月底才通知,心里想骂,但是真的好开心好开心,黑暗中的光芒,泪奔。。。。约到了6月15号面试。 6月初开始自己的回家倒计时,请那唯一一个跟我好的妹纸吃饭,忙完答辩,然后是优秀论文的小报告,13号下午交了上去,再回到宿舍,空无一人,一遍忍着不要悲伤,一遍收拾东西,然后向每个宿舍敲门,告别,一个人拖着行李箱,嗯,我要淡定。
准备了有点久,我以为自己准备好了,15号,深圳,珍爱网,被问到:有没有JVM调优经验?ZK是如何保持一致性的?有没有写过ELK索引经验?谈谈你对dubbo的理解。。。事后,得知自己没有过。心态真的炸了,通宵打了两天游戏。月底得知,优秀论文也没有被评上,已经没有骂人吐槽的想法,只是哦的一声,6月,宣告了自己春招结束,宣告自己要去外包,同时也宣告自己大学这些努力全部白费,跟那个灌毒鸡汤天天打游戏的人同一起跑线。
七月初入职的时候,问了问那些一起应届入职的同学,跟别人交流一下面试被问的问题,只有线程池,JVM回收算法,他们也没用过,我无奈,为什么我被问到的,全是Angularjs、MD5加密算法、ZK一致性原理这种奇葩的问题。。。。。
没有环境,就自己创造,下班有时间就改改自己的网站http://www.wenzhihuai.com,GitHub目前有154个star了,感觉也是个小作品吧,把常用的Java技术栈(RabbitMQ、MongoDB等)加了上去,虽然没啥使用场景,但是自己好好努力吧,毕竟高并发、高可用是自己的梦想。今年GitHub自己的提交次数。
决心先好好呆着,学习学习,把公司的产品业务技术专研专研,7、8月份是挺开心的,没怎么加班,虽然下班我都留在公司学习,虽然公司的产品不咋地,但是还是有可学之处的,随后,我没想到的是,公司要实行996,,,9月份至12月,每天都是改bug,开发模块,写业务,全都是增删改查,没有涉及redis、rabbitmq,就真的只有使用Hibernate、spring,而做出的东西,要使用JDK6来运行,至今都能看到sun的类。每天的状态就是累累累,我们组还不是最惨的,隔壁项目组,一周通宵两次,9月到10都是如此,国庆加班,每次经过他们组,我都觉得自己还算是幸福。很厌恶这种不把员工当人看的行为,有同事调侃,明年估计我们这些应届过来的,是不是要走掉2/3的人?疯狂加班的代价是牺牲积极性,我问过那些一周通宵两次的人,敲代码,程序员你喜欢这份工作么?不喜欢!!!有那么一两次,实在是太累了,我都感觉自己有点不行了,想离职,可是,会有公司要我么?会有公司不问那些奇葩的问题么?
双十一,花了500多买了十本书,神书啊,计算机届的圣经啊,然而至今一本都没看完,甚至是都没怎么翻,每天下班回去之后就是9点半了,烧水,洗澡,洗衣服,近11点了,躺床上,一遍看书一遍看鹌鹑直播,一小时才翻几页,996真的太累了,实在看不下书。经过这几个月的加班,感觉自己技术增长为负数,也病了几次,同学见我,都感觉我老了不少,心里不平,可是又没有什么办法。
珠江新城,一年四季都那么美,今晚应该又是万人一起倒计时了吧?已经好久没跟别人一起出去玩过了。今年貌似得了自闭症?社交全靠黄段子。想找人出门走走,然而手机已经没有那种几年的朋友,大学同学一个都不在,哈哈哈,别人说社交软件让人离得更近,我怎么发现我自己一个好朋友都没有了呢,哭笑ing。或许是自己的确不善于跟别人保持联系吧,这个要改,一定要改!!! 远看:
近看:
题外话: 有些时候,遇到不顺,感觉自己不是神,没必要忍住,就开始宣泄自己的情绪,比如疯狂打游戏,只能这样度过了吧。
今年听过最多的一句话,你好厉害,你好牛逼==我感觉也是,可惜,要被业务耽误了不少时间吧。。
《企业IT架构转型之道》是今年最好的架构图书,没有之一。
想起初中,老师让我在黑板写自己的英文作文,好像是信件,描述什么状况来这?最后夸了夸我在文章后面加的一段大概是劝别人不要悲伤要灿烂。也想起一个故事,二战以后,德国满目疮痍,他们处在这样一个凄惨的境地,还能想到在桌上摆设一些花,就一定能在废墟上重建家园。我所看到的是即使在再黑暗的状态下,要永远抱着希望。明年要更加努力的学习,而不是工作,是该挽留一些人了,多运动。说了那么多,其实,我只希望自己的运气不要再差了,再差就没法活了。人在遇到不顺的时候,往往最难过的一关便是自己。哈哈哈,只能学我那个同学给自己灌毒鸡汤了。加油加油↖(^ω^)↗
年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。
想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。
其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。 去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,面经在这,之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。
and ... 广州拜拜,晚上人少的时候,跟妹纸去珠江新城真的很美好
入职的时候是做日志收集的,就是flume+kafka+es这一套,遇到了不少大佬,嗯,感觉挺好的,打算好好干,一个半月后不知怎么滴,被拉去做容器云了,然后就开始了天天调jenkins、gitlab这些没什么用的api,开始了增删改查的历程,很烦,研究jenkins、gitlab对于自己的技术没什么提升,又不得不做,而且大小周+加班这么做,回到家自己看java、netty、golang的书籍,可是没项目做没实践,过几个月就忘光了,有段时间真的很烦很烦,根本不想工作,天天调api,而且是jenkins、gitlab这些没什么卵用的api,至今也没找到什么办法。。。。 公司发展太快,也实在让人不知所措,一不小心部门被架空了,什么预兆都没有,被分到其他部门,然后发现这个部门领导内斗真的是,“三国争霸”吧,无奈成为炮灰,不同的领导不同的要求,安安静静敲个代码怎么这么难。。。。 去年的学习拉下了不少,文章也写的少了,总写那些入门的文章确实没有什么意思,买的书虽然没有17年的多吧,不过也不少了,也看了挺多本书的,虽然我还是不满意自己的阅读量。 这几年要开始抓准一个框架了,好好专研一下,也要开始学习一下go了,希望能参与一些github的项目,好好努力吧。
平时没事总是宅在家里打游戏,差不多持续到下半年吧,那会打游戏去楼下吃东西的时候老是觉得眼神恍惚,盯着电子产品太多了,然后就跟别人跑步去了,每周去一次人才公园,跑步健身,人才公园真的是深圳跑步最好的一个地方了,桥上总有一群摄影师,好想在深圳湾买一套房子,看了看房价,算了算了,深圳的房,真的贵的离谱。19年也要多多运动,健康第一健康第一。
想不起来还有什么要说的了,毕竟程序员,好像每天的生活都一样,就展望一下19年吧。今年,最最重要的是家里人以及身边还有所有人都健健康康的,哈哈哈。然后是安安静静的敲代码~~就酱
2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。
从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。
之后有了点起色,然后开始着重强调了DevOps的重要性,期初也没人理解,还被骂天天研究技术不干活。。。这东西没技术,但是真的是公司十分需要的公司好么!!可惜公司对这个产品很失望,也不给人,期间还说要暂停项目,唉,这么大的项目就给几个人怎么做,谁都不乐意,就这么过的很憋屈吧,开始了维护的日子。。。
工作上,蛮多人对我挺好的,领导,同事,不过有时候憋屈的时候还是有了点情绪化,有时候有点悲伤的想法,浮躁、暴躁,出去面试了几次,感觉不是自己能力不好,而是市场上的真的需要3+年以上的。。。。不过感觉自己能在这里憋屈的过了这么久,也是很佩服自己的,哈哈,用同事的话来说就是:当做游戏里的练级,锻炼自己哈哈哈
项目的方向错误,导致自己写的代码都是很简单的增删改查,没有技术含量的那种,也最终导致自己浪费了不少时间,上半年算是很气愤吧,不想就这么浪费掉这些时间,虽然期间看了各种各样的东西,比如Netty、Go、操作系统什么的,最终发现,如果不在项目中使用的话,真的会忘,而且很快就忘掉,最后的还是决定学习项目相关度比较大的东西了,比如Go,如果项目不是很大,小项目的话,就用Go来写,这么做感觉提升还是挺大的。 过去一年往南山图书馆借了不少书,省了不少钱,虽然没怎么看,但我至少有个奋斗的心啊,哈哈哈哈哈哈哈哈,文章写得少了,因为感觉写不出好东西,总是入门的那种,不深,不值得学习,想想到时候把别人给坑了,也不好意思。 操作系统感觉还是要好好学学了,加油
Kubernetes就是未来的方向,有时候翻开源码看看,又不敢往下面看了,对底层不熟吧,今年要多多研究下Kubernetes的源码了,至于Spring那一套,也不知道该不该放弃,或者都学习一下?云原生就是趋势。
吵架了,没心思写
过去一年还是把运动算是坚持了下来,每个月必去深圳湾跑一次。还是没怎么睡好,工作感情上都有点不顺,加上自己本身就难以入睡,有时候躺床上就是怎么也睡不着,还长了痘痘,要跑去医院那种,可怕,老了,还是要早点睡觉,多走走多运动,好久没打羽毛球了,自己也不想有空的时候只会打游戏,今年继续加油,多运动吧。
还爬了两次南山
看了周围蛮多同事去了腾讯阿里,有点心动,好想去csig,没到3年真的让人很抓狂。 过去一年过的蛮憋屈的,特别是工作不顺,加上跟女朋友吵架,心态爆炸。。。
每年这个时候,都不敢有太大的期望了,祝大家都健健康康的,工作顺利!!
当然,如果有可能的话,我想去CSIG
1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。
2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。
3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②单机吞吐量:十万级,最大的优点,就是吞吐量高; ③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源; ④时效性:ms级; ⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用; ⑥消息可靠性:经过参数优化配置,消息可以做到0丢失; ⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。
缺点: ①由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序; ②有可能消息重复消费; ③依赖zookeeper进行元数据管理,等等。
4、为什么说Kafka性能很好,体现在哪里? ①顺序读写 ②零拷贝 ③分区 ④批量发送 ⑤数据压缩
5、请说明什么是传统的消息传递方法? 传统的消息传递方法包括两种: 排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。 发布-订阅:在这个模型中,消息被广播给所有的用户。
6、请说明Kafka相对传统技术有什么优势? ①快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。 ②可伸缩:在一组机器上对数据进行分区 ③和简化,以支持更大的数据 ④持久:消息是持久性的,并在集群中进 ⑤行复制,以防止数据丢失。 ⑥设计:它提供了容错保证和持久性
7、解释Kafka的Zookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka吗? Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。 不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。 Zookeeper主要用于在集群中不同节点之间进行通信 在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取 除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。
8、解释Kafka的用户如何消费信息? 在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。
9、解释如何提高远程用户的吞吐量? 如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。
10、解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息? 在数据中,为了精确地获得Kafka的消息,你必须遵循两件事: 在数据消耗期间避免重复,在数据生产过程中避免重复。 这里有两种方法,可以在数据生成时准确地获得一个语义: 每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功 在消息中包含一个主键(UUID或其他),并在用户中进行反复制
11、解释如何减少ISR中的扰动?broker什么时候离开ISR? ISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会从ISR中删除。
12、Kafka为什么需要复制? Kafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。
13、如果副本在ISR中停留了很长时间表明什么? 如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。
14、请说明如果首选的副本不在ISR中会发生什么? 如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。
15、有可能在生产后发生消息偏移吗? 在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。
作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。
16、Kafka的设计时什么样的呢? Kafka将消息以topic为单位进行归纳 将向Kafka topic发布消息的程序成为producers. 将预订topics并消费消息的程序成为consumer. Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker. producers通过网络将消息发送到Kafka集群,集群向消费者提供消息
17、数据传输的事物定义有哪三种? (1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输 (2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输. (3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的
18、Kafka判断一个节点是否还活着有那两个条件? (1)节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接 (2)如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久
19、producer是否直接将数据发送到broker的leader(主节点)? producer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发,为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。
20、Kafa consumer是否可以消费指定分区消息? Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的
21、Kafka消息是采用Pull模式,还是Push模式? Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式
Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略
Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发
22、Kafka存储在硬盘上的消息格式是什么? 消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。 消息长度: 4 bytes (value: 1+4+n) 版本号: 1 byte CRC校验码: 4 bytes 具体的消息: n bytes
23、Kafka高效文件存储设计特点: (1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。 (2).通过索引信息可以快速定位message和确定response的最大大小。 (3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。 (4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
24、Kafka 与传统消息系统之间有三个关键区别 (1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留 (2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性 (3).Kafka 支持实时的流式处理
25、Kafka创建Topic时如何将分区放置到不同的Broker中 副本因子不能大于 Broker 的个数; 第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的; 其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推; 剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的
26、Kafka新建的分区会在哪个目录下创建 在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。 但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
27、partition的数据如何保存到硬盘 topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增, 且消息有序 Partition文件下有多个segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名
28、kafka的ack机制 request.required.acks有三个值 0 1 -1 0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据 1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失 -1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失
29、Kafka的消费者如何消费数据 消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费。同时也可以按照指定的offset进行重新消费。
30、消费者负载均衡策略 结合consumer的加入和退出进行再平衡策略。
31、kafka消息数据是否有序? 消费者组里某具体分区是有序的,所以要保证有序只能建一个分区,但是实际这样会存在性能问题,具体业务具体分析后确认。
32、kafaka生产数据时数据的分组策略,生产者决定数据产生到集群的哪个partition中 每一条消息都是以(key,value)格式 Key是由生产者发送数据传入 所以生产者(key)决定了数据产生到集群的哪个partition
33、kafka consumer 什么情况会触发再平衡reblance? ①一旦消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中的所有消费者都要执行再平衡。 ②订阅主题分区发生变化,所有消费者也都要再平衡。
34、描述下kafka consumer 再平衡步骤? ①关闭数据拉取线程,情空队列和消息流,提交偏移量; ②释放分区所有权,删除zk中分区和消费者的所有者关系; ③将所有分区重新分配给每个消费者,每个消费者都会分到不同分区; ④将分区对应的消费者所有关系写入ZK,记录分区的所有权信息; ⑤重启消费者拉取线程管理器,管理每个分区的拉取线程。
35、Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么 36、Kafka中的HW、LEO、LSO、LW等分别代表什么? 37、Kafka中是怎么体现消息顺序性的? 38、Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么? 39、Kafka生产者客户端的整体结构是什么样子的? 40、Kafka生产者客户端中使用了几个线程来处理?分别是什么? 41、Kafka的旧版Scala的消费者客户端的设计有什么缺陷? 42、“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段? 43、消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1? 44、有哪些情形会造成重复消费? 45、那些情景下会造成消息漏消费? 46、KafkaConsumer是非线程安全的,那么怎么样实现多线程消费? 47、简述消费者与消费组之间的关系 48、当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑? 49、topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么? 50、topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么? 51、创建topic时如何选择合适的分区数? 52、Kafka目前有那些内部topic,它们都有什么特征?各自的作用又是什么? 53、优先副本是什么?它有什么特殊的作用? 54、Kafka有哪几处地方有分区分配的概念?简述大致的过程及原理 55、简述Kafka的日志目录结构 56、Kafka中有那些索引文件? 57、如果我指定了一个offset,Kafka怎么查找到对应的消息? 58、如果我指定了一个timestamp,Kafka怎么查找到对应的消息? 59、聊一聊你对Kafka的Log Retention的理解 60、聊一聊你对Kafka的Log Compaction的理解 61、聊一聊你对Kafka底层存储的理解(页缓存、内核层、块层、设备层) 62、聊一聊Kafka的延时操作的原理 63、聊一聊Kafka控制器的作用 64、消费再均衡的原理是什么?(提示:消费者协调器和消费组协调器) 65、Kafka中的幂等是怎么实现的 66、Kafka中的事务是怎么实现的(这题我去面试6家被问4次,照着答案念也要念十几分钟,面试官简直凑不要脸。实在记不住的话...只要简历上不写精通Kafka一般不会问到,我简历上写的是“熟悉Kafka,了解RabbitMQ....”) 67、Kafka中有那些地方需要选举?这些地方的选举策略又有哪些? 68、失效副本是指什么?有那些应对措施? 69、多副本下,各个副本中的HW和LEO的演变过程 70、为什么Kafka不支持读写分离? 71、Kafka在可靠性方面做了哪些改进?(HW, LeaderEpoch) 72、Kafka中怎么实现死信队列和重试队列? 73、Kafka中的延迟队列怎么实现(这题被问的比事务那题还要多!!!听说你会Kafka,那你说说延迟队列怎么实现?) 74、Kafka中怎么做消息审计? 75、Kafka中怎么做消息轨迹? 76、Kafka中有那些配置参数比较有意思?聊一聊你的看法 77、Kafka中有那些命名比较有意思?聊一聊你的看法 78、Kafka有哪些指标需要着重关注? 79、怎么计算Lag?(注意read_uncommitted和read_committed状态下的不同) 80、Kafka的那些设计让它有如此高的性能? 81、Kafka有什么优缺点? 82、还用过什么同质类的其它产品,与Kafka相比有什么优缺点? 83、为什么选择Kafka? 84、在使用Kafka的过程中遇到过什么困难?怎么解决的? 85、怎么样才能确保Kafka极大程度上的可靠性? 86、聊一聊你对Kafka生态的理解
留空
jfaowejfoewj
想体验的可以去微信上搜索【旅行的树】公众号。
openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。
国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。
国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。
之后再搜索框填openai进行下单购买即可。
openai在国内不提供服务的,而且也通过ip识别是不是在国内,解决办法用vpn也行,或者,自己去买一台国外的服务器也行。我这里使用的是腾讯云轻量服务器,最低配置54元/月,选择windows的主要原因毕竟需要注册openai,需要看页面,同时也可以搭建nginx,当然,用ubuntu如果能自己搞界面也行。
购买完之后,就可以直接打开openai的官网了,然后去https://platform.openai.com/signup官网里注册,注册过程具体就不讲了,讲下核心问题——短信验证码
然后回sms查看验证码。
注册成功之后就可以在chatgpt里聊天啦,能够识别各种语言,发起多轮会话的时候,可能回出现访问超过限制什么的。
通过chatgpt聊天不是我们最终想要的,我们需要的是在微信公众号也提供智能客服的聊天回复,所以我们需要在通过openai的api来进行调用。
跟页面一样,OpenAI的调用也是不能再国内访问的,这里,我们使用同一台服务器来搭建nginx,还是保留使用windows吧,主要还是得注意下面这段话,如果API key被泄露了,OpenAI可能会自动重新更新你的API key,这个规则似乎是API key如果被多个ip使用,就会触发这个规则,调试阶段还是尽量使用windows的服务器吧,万一被更新了,还能去页面上重新找到。
Do not share your API key with others, or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may also automatically rotate any API key that we've found has leaked publicly.
+
windows的安装过程参考网上的来,我们只需要添加下面这个配置即可,原理主要是将调用OpenAI的接口全部往官网转发。
location /v1/completions {
+ proxy_pass https://api.openai.com/v1/completions;
+ }
+
然后使用下面的方法进行调试即可:
POST http://YOUR IP/v1/completions
+Authorization: Bearer YOUR_API_KEY
+Content-Type: application/json
+
+{
+ "model": "text-davinci-003",
+ "prompt": "Say this is a test",
+ "max_tokens": 7,
+ "temperature": 0
+}
+
网上有很多关于微信通过chatgpt回复的文章,有些使用自己微信号直接做为载体,因为要扫码网页登陆,而且是网页端在国外,很容易被封;有些是使用公众号,相对来说,公众号被封也不至于导致个人微信号出问题。
微信公众平台提供了微信云托管,无需鉴权,比其他方式都方便不少,可以免费试用3个月,继续薅羊毛,当然,如果自己开发能力足够,也可以自己从0开始开发。
提供了各种语言的模版,方便快速开发,OpenAI官方提供的sdk是node和python,这里我们选择express(node)。
微信官方的源码在这,https://github.com/WeixinCloud/wxcloudrun-express,我们直接fork一份自己来开发。
一个简单的消息回复功能(无db),直接在我们的index.js里添加如下代码。
const configuration = new Configuration({
+ apiKey: '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',
+ basePath: 'http://43.153.15.174/v1'
+});
+const openai = new OpenAIApi(configuration);
+
+async function simpleResponse(prompt) {
+ const completion = await openai.createCompletion({
+ model: 'text-davinci-003',
+ prompt,
+ max_tokens: 1024,
+ temperature: 0.1,
+ });
+ const response = (completion?.data?.choices?.[0].text || 'AI 挂了').trim();
+ return strip(response, ['\n', 'A: ']);
+}
+
+app.post("/message/simple", async (req, res) => {
+ console.log('消息推送', req.body)
+ // 从 header 中取appid,如果 from-appid 不存在,则不是资源复用场景,可以直接传空字符串,使用环境所属账号发起云调用
+ const appid = req.headers['x-wx-from-appid'] || ''
+ const {ToUserName, FromUserName, MsgType, Content, CreateTime} = req.body
+ console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)
+ if (MsgType === 'text') {
+ message = await simpleResponse(Content)
+ res.send({
+ ToUserName: FromUserName,
+ FromUserName: ToUserName,
+ CreateTime: CreateTime,
+ MsgType: 'text',
+ Content: message,
+ })
+ } else {
+ res.send('success')
+ }
+})
+
本地可以直接使用http请求测试,成功调用之后直接提交发布即可,在微信云托管上需要先授权代码库,即可使用云托管的流水线,一键发布。注意,api_base和api_key可以填写在服务设置-基础信息那里,一个是OPENAI_API_BASE,一个是OPENAI_API_KEY,服务会自动从环境变量里取。
提交代码只github或者gitee都可以,值得注意的是,OpenAI判断key泄露的规则,不知道是不是判断调用的ip地址不一样,还是github的提交记录里含有这块,有点玄学,同样的key本地调用一次,然后在云托管也调用的话,OpenAI就很容易把key给重新更新。
部署完之后,云托管也提供了云端调试功能,相当于在服务里发送了http请求。这一步很重要,如果没有调用成功,则无法进行云托管消息推送。
这里填上你自己的url,我们这里配置的是/meesage/simple,如果没有成功,需要进行下面步骤进行排查:
(1)服务有没有正常启动,看日志
(2)端口有没有设置错误,这个很多次没有注意到
保存成功之后,就可以在微信公众号里测试了。
体验还可以
很多OpenAI的回答都要几十秒,有的甚至更久,比如对chatgpt问“写一篇1000字关于深圳的文章”,就需要几十秒,而微信的主动回复接口,是需要我们3s内返回给用户。
订阅号的消息推送分几种:
根据微信官方文档,没有认证的公众号是没有调用主动回复接口权限的,https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html
对于有微信认证的订阅号或者服务号,可以调用微信官方的/cgi-bin/message/custom/send接口来实现主动回复,但是对于个人的公众号,没有权限调用,只能尝试别的办法。想来想去,只能在3s内返回让用户重新复制发送的信息,同时后台里保存记录异步调用,用户重新发送的时候再从数据库里提取回复。
1.先往数据库存一条 回复记录,把用户的提问存下来,以便后续查询。设置回复的内容为空,设置状态为 回复中(thinking)。
// 因为AI响应比较慢,容易超时,先插入一条记录,维持状态,待后续更新记录。
+ await Message.create({
+ fromUser: FromUserName,
+ response: '',
+ request: Content,
+ aiType: AI_TYPE_TEXT, // 为其他AI回复拓展,比如AI作画
+ });
+
2.抽象一个 chatGPT 请求方法 getAIMessage
,函数内部得到 GPT 响应后,会更新之前那条记录(通过用户id & 用户提问 查询),把状态更新为 已回答(answered),并把回复内容更新上。
// 成功后,更新记录
+ await Message.update(
+ {
+ response: response,
+ status: MESSAGE_STATUS_ANSWERED,
+ },
+ {
+ where: {
+ fromUser: FromUserName,
+ request: Content,
+ },
+ },
+ );
+
3.前置增加一些判断,当用户在请求时,如果 AI 还没完成响应,直接回复用户 AI 还在响应,让用户过一会儿再重试。如果 AI 此时已响应完成,则直接把 内容返回给用户。
// 找一下,是否已有记录
+ const message = await Message.findOne({
+ where: {
+ fromUser: FromUserName,
+ request: Content,
+ },
+ });
+
+ // 已回答,直接返回消息
+ if (message?.status === MESSAGE_STATUS_ANSWERED) {
+ return `[GPT]: ${message?.response}`;
+ }
+
+ // 在回答中
+ if (message?.status === MESSAGE_STATUS_THINKING) {
+ return AI_THINKING_MESSAGE;
+ }
+
4.最后就是一个 Promise.race
const message = await Promise.race([
+ // 3秒微信服务器就会超时,超过2.9秒要提示用户重试
+ sleep(2900).then(() => AI_THINKING_MESSAGE),
+ getAIMessage({ Content, FromUserName }),
+ ]);
+
这样子大概就能实现超时之前返回了。
掉接口是一次性的,一次接口调用完之后怎么做到下一次通话的时候,还能继续保持会话,是不是应该类似客户端与服务端那种有个session这种,但是实际上在openai里是没有session这种东西的,令人震惊的是,chatgpt里是通过前几次会话拼接起来一起发送给chatgpt里的,需要通过回车符来拼接。
async function buildCtxPrompt({ FromUserName }) {
+ // 获取最近10条对话
+ const messages = await Message.findAll({
+ where: {
+ fromUser: FromUserName,
+ aiType: AI_TYPE_TEXT,
+ },
+ limit: 10,
+ order: [['updatedAt', 'ASC']],
+ });
+ // 只有一条的时候,就不用封装上下文了
+ return messages.length === 1
+ ? messages[0].request
+ : messages
+ .map(({ response, request }) => `Q: ${request}\n A: ${response}`)
+ .join('\n');
+}
+
之后就可以实现会话之间的保存通信了。
chatgpt毕竟也是新上线的,火热是肯定的,聊天窗口只能开几个,api调用的话,也是有限频的,但是规则具体没有找到,只是在调用次数过多的时候会报429的错误,出现之后就需要等待一个小时左右。
对于这个的解决办法只能是多开几个账号,一旦429就只能换个账号重试了。
没有找到详细的规则,凭个人经验的话,可能github提交的代码会被扫描,可能ip调用的来源不一样,最好还是开发一个秘钥,生产一个秘钥吧。
我们这里用的模型算法是text-davinci-003,具体可以参考:https://platform.openai.com/docs/models/overview,也算是一个比较老的样本了吧
从官方文档来看,官方服务版的 ChatGPT 的模型并非基础版的text-davinci-003
,而是经过了「微调:fine-tunes」。文档地址在这:platform.openai.com/docs/guides…
有时候消息没有回复,真的不是我们的问题,chatgpt毕竟太火了,官网的这个能力都经常挂掉,也可以订阅官网修复的通知,一旦修复则会发邮件告知你。
参考:https://juejin.cn/post/7200769439335546935
记得去微信关注【旅行的树】公众号体验
代码地址:https://github.com/Zephery/wechat-gpt
如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。
正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.
注意点:(1)不要用自己车辆的邮箱来注册(2)有些邮箱不是特斯拉开发者的邮箱,可能用这些有些无法正常提交访问请求。
点击下方的“提交请求”按钮,请求应用程序访问权限。登录后,请提供您的合法企业详细信息、应用程序名称及描述和使用目的。在您提交了详细信息之后,我们会审核您的请求并通过电子邮件向您发送状态更新。
这一步很坑,多次尝试之后都无果,原因也不知道是为啥,只能自己去看返回报文琢磨,太难受了,下面是自己踩的坑
(1)Invalid domain
无效的域名,这里我用的域名是腾讯云个人服务器的域名,证书是腾讯云免费一年的证书,印象中第一申请的时候还是能过的,第二次的时候就不行了,可能被识别到免费的ssl证书不符合规范,还是需要由合法机构的颁发证书才行。所以,为了金快速申请通过,先填个https://baidu.com吧。当然,后续需要彻底解决自己域名证书的问题,我改为使用阿里云的ssl证书,3个月到期的那种。
(2)Unable to Onboard
应用无法上架,可能原因为邮箱不对,用了之前消费者账号(即自己的车辆账号),建议换别的邮箱试试。
(3) Rejected
这一步尝试了很多次,具体原因为国内还无法正常使用tesla api,只能切换至美国服务器申请下(截止2023-11-15),后续留意官网通知。
一旦获得批准,将为您的应用程序生成可在登录后访问的客户端 ID 和客户端密钥。使用这些凭据,通过 OAuth 2.0 身份验证获取用户访问令牌。访问令牌可用于对提供私人用户账户信息或代表其他账户执行操作的请求进行身份验证。
申请好就可以在自己的账号下看到自己的应用了,
按照 API 文档和设置说明将您的应用程序与 Tesla 车队 API 集成。您需要生成并注册公钥,请求用户授权并按照规格要求拨打电话。完成后您将能够与 API 进行交互,并开始围绕 Tesla 设备构建集成。
由于特斯拉刚推出,并且国内进展缓慢,很多都申请不下来,下面内容均以北美区域进行调用。
app与特斯拉交互的共有两个令牌(token)方式,在调用api的时候,特别需要注意使用的是哪种token,下面是两种token的说明:
(1)合作伙伴身份验证令牌:这个就是你申请的app的令牌
(2)客户生成第三方令牌:这个是消费者在你这个app下授权之后的令牌。
此外,还需要注意下,中国大陆地区对应的api地址是 https://fleet-api.prd.cn.vn.cloud.tesla.cn,不要调到别的地址去了。
这一步官网列的很详细,就不在详述了。
CLIENT_ID=<command to obtain a client_id>
+CLIENT_SECRET=<secure command to obtain a client_secret>
+AUDIENCE="https://fleet-api.prd.na.vn.cloud.tesla.com"
+# Partner authentication token request
+curl --request POST \
+ --header 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode 'grant_type=client_credentials' \
+ --data-urlencode "client_id=$CLIENT_ID" \
+ --data-urlencode "client_secret=$CLIENT_SECRET" \
+ --data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \
+ --data-urlencode "audience=$AUDIENCE" \
+ 'https://auth.tesla.com/oauth2/v3/token'
+
+
完成注册合作方账号之后才可以访问API. 每个developer.tesla.cn上的应用程序都必须完成此步骤。官网挂了一个github的代码,看了半天,以为要搞很多东西,实际上只需要几步就可以了。
cd vehicle-command/cmd/tesla-keygen
+go build .
+./tesla-keygen -f -keyring-debug -key-file=private create > public_key.pem
+
这里只是生成了公钥,需要把公钥挂载到域名之下,我们用的是nginx,所以只要指向pem文件就可以了,注意下nginx是否可以访问到改文件,如果不行,把nginx的user改为root。
location ~ ^/.well-known {
+ default_type text/html;
+ alias /root/vehicle-command/cmd/tesla-keygen/public_key.pem;
+ }
+
随后,便是向tesla注册你的域名,域名必须和你申请的时候填的一样。
curl --header 'Content-Type: application/json' \
+ --header "Authorization: Bearer $TESLA_API_TOKEN" \
+ --data '{"domain":"string"}' \
+ 'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts'
+
+
最后,验证一下是否真的注册成功(GET /api/1/partner_accounts/public_key)
curl --header 'Content-Type: application/json' \
+ --header "Authorization: Bearer $TESLA_API_TOKEN" \
+ 'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts/public_key'
+
得到下面内容就代表成功了
{"response":{"public_key":"xxxx"}}
+
至此,我们的开发准备就完成了,接下来就是正常的开发与用户交互的api。
各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。 大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。
由原本的ssh变成ssm,再变成ssm+shiro+lucene,到现在的前后台分离。前台使用ssm+lucene,后台使用spring boot+shiro。其中,使用pagehelper作为分页,lucene用来搜索和自动补全,使用百度统计的API做了个日志系统,统计pv和uv什么的,同时,还有使用了JMX来观察JVM的使用和cpu的使用率,机器学习方面,使用了adaboost和朴素贝叶斯对微博进行分类,有兴趣的可以点点有点意思这个页面。 本文从下面这几个方面来讲讲网站的建立:
起初,是因为学习的时候老是找不到什么好玩而又有挑战性的项目,看着struts、spring、hibernate的书,全都是一些小项目,做了感觉也没啥意义,有时候在博客园看到别人还有自己的网站,特羡慕,最终就选择了自己做一个个人网站。期初刚学的ssh,于是开始了自己的ssh搭建个人网站的行程,但是对于一个后端的人来说,前端是个大问题啊。。。。所以初版的时候差不多全是纯生的html、css、js,至少发博客这个功能实现了,但是确实没法看。前前后后折腾了一个多月,决定推翻重做,到百度看看别人怎么做的。首先看到的是杨青的网站,已经好几年没更新了,前端的代码看起来比较简单,也是自己能够掌握的,但是不够美观,继续找,在模板之家发现了一个高大上的模板。
第二版的界面确实是这样的,只是把图片的切换变成了wowslider,也是简单的用bootstrap和pagehelper做了下分页,现在的最终版保留了它的header,然后评论框使用了多说(超级怀念多说)。后端也由原来的ssh变成了ssm,之后加上了lucene来对文章进行索引。之后,随着多说要关闭了,突然之间有很多div都不适应了(我写死了的。。。),再一次,没法看,不想看,一怒之下再次推翻重做,变成了现在这个版本。
最终版本在考虑时,也找了很多模板,影响深刻的是tale和欲思这两个主题,期中,tale使用的是java语言写的,刚知道的那一刻我就没好感了,java后端我是要自己全部写的,tale这个页面简洁但是不够炫,而且内容量太低,可能就只是个纯博客,之后发现了欲思,拓展性强,只可惜没有静态的版本,后台是纯生的PHP(嗯,PHP是世界上最好的语言),看了看,没事,保存网页,前端自己改,后端自己全部重写,最终变成了现在这个版本,虽然拼接的时候各种css、js混入。。。。还好在做网站性能优化的时候差不多全部去掉了。最终版加入redis、quartz、shiro等,还有python机器学习、flask的restful api,可谓是大杂烩了。 页面看着还算凑合,至少不是那种看都看不过去的那种了,但是仔细看看,还是有不少问题的,比如瀑布流,还有排版什么的。只能等自己什么时候想认真学学前端的东西了。 已经部署在腾讯云服务器上,存储使用了七牛云的cdn。
最终版的技术架构图如下:
网站核心主要采用Spring SpringMVC和Mybatis,下图是当访问一篇博客的时候的运行流程,参考了张开涛的博客。
运行流程分析
日志系统架构如下:
日志系统曾经尝试采用过ELK,实时监控实在是让人不能不称赞,本地也跑起来了,但是一到服务器,卡卡卡,毕竟(1Ghz CPU、1G内存),只能放弃ELK,采用百度统计。百度统计提供了Tongji API供开发者使用,只是有次数限制,2000/日,实时的话实在有点低,只能统计前几天的PV、UV等开放出来。其实这个存放在mysql也行,不过这些零碎的数据还是放在redis中,方便管理。 出了日志系统,自己对服务器的一些使用率也是挺关心的,毕竟服务器配置太低,于是利用了使用了tomcat的JMX来对CPU和jvm使用情况进行监控,这两个都是实时的。出了这两个,对内存的分配做了监控,Eden、Survivor、Tenured的使用情况。
本人大学里的毕业设计就是基于AdaBoost算法的情感分类,学到的东西还是要经常拿出来看看,要不然真的浪费了我这么久努力做的毕业设计啊。构建了一个基本的情感分类小系统,每天抓取微博进行分类存储在MySql上,并使用flask提供Restful API给java调用,可以点击这里尝试(请忽略Google的图片)。目前分类效果不是很明显,准确率大概只有百分之70%,因为训练样本只有500条(找不到训练样本),机器学习真的太依赖样本的标注。这个,只能请教各位路人大神指导指导了。
断断续续,也总算做了一个能拿得出手仍要遮遮掩掩才能给别人看的网站,哈哈哈哈哈,也劳烦各位帮忙找找bug。前前后后从初学Java EE就一直设想的事也还算有了个了结,以后要多多看书,写点精品文章。PS:GitHub上求给个Star,以后面试能讲讲这个网站。
因原本的
首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。
倒排索引:将文档中的词作为关键字,建立词与文档的映射关系,通过对倒排索引的检索,可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。
停用词:在一段句子中,去掉之后对句子的表达意向没有印象的词语,如“非常”、“如果”,中文中主要包括冠词,副词等。
排序:搜索引擎在对一个关键词进行搜索时,可能会命中许多文档,这个时候,搜索引擎就需要快速的查找的用户所需要的文档,因此,相关度大的结果需要进行排序,这个设计到搜索引擎的相关度算法。
索引的建立
索引的搜索
注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一样。首先需要导入lucene所使用的包。使用maven:
<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-core</artifactId><!--lucene核心-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-analyzers-common</artifactId><!--分词器-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-queryparser</artifactId><!--格式化-->
+ <version>${lucene.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
+ <version>${lucene.version}</version>
+</dependency>
+
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存储的位置
+SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器
+IndexWriterConfig config = new IndexWriterConfig(analyzer);
+IndexWriter writer = new IndexWriter(dir, config);
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //对标题做索引
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引
+writer.addDocument(doc);
+writer.close();
+
IndexWriter writer = getWriter();
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
+writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc); //更新索引
+writer.close();
+
private static void search_index(String keyword) {
+ try {
+ Directory dir = FSDirectory.open(Paths.get("blog_index")); //获取要查询的路径,也就是索引所在的位置
+ IndexReader reader = DirectoryReader.open(dir);
+ IndexSearcher searcher = new IndexSearcher(reader);
+ SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+ QueryParser parser = new QueryParser("content", analyzer); //查询解析器
+ Query query = parser.parse(keyword); //通过解析要查询的String,获取查询对象
+ TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中,
+ for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+ Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+ System.out.println(doc.get("title")); //fullPath是刚刚建立索引的时候我们定义的一个字段
+ }
+ reader.close();
+ } catch (IOException | ParseException e) {
+ logger.error(e.toString());
+ }
+}
+
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
+SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
+Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
+highlighter.setTextFragmenter(fragmenter);
+for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+ Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+ String title = doc.get("title");
+ TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
+ String hTitle = highlighter.getBestFragment(tokenStream, title);
+ System.out.println(hTitle);
+}
+
结果
<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
+
ScoreDoc lastBottom = null;//相当于pageSize
+BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
+QueryParser parser1 = new QueryParser("title", analyzer);//对文章标题进行搜索
+Query query1 = parser1.parse(q);
+booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
+TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits); //lastBottom(pageSize),pagehits(pagenum)
+
百度、谷歌等在输入文字的时候会弹出补全框,如下图:
在搭建lucene自动补全的时候,也有考虑过使用SQL语句中使用like来进行,主要还是like对数据库压力会大,而且相关度没有lucene的高。主要使用了官方suggest库以及autocompelte.js这个插件。 suggest的原理看这,以及索引结构看这。
<dependency>
+ <groupId>org.apache.lucene</groupId>
+ <artifactId>lucene-suggest</artifactId>
+ <version>6.6.0</version>
+</dependency>
+
public class Blog implements Serializable {
+
InputIterator的几个方法:
+long weight():返回的权重值,大小会影响排序,默认是1L
+BytesRef payload():对某个对象进行序列化
+boolean hasPayloads():是否有设置payload信息
+Set<BytesRef> contexts():存入context,context里可以是任意的自定义数据,一般用于数据过滤
+boolean hasContexts():判断是否有下一个,默认为false
+
public class BlogIterator implements InputIterator {
+ /**
+ * logger
+ */
+ private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
+ private Iterator<Blog> blogIterator;
+ private Blog currentBlog;
+
+ public BlogIterator(Iterator<Blog> blogIterator) {
+ this.blogIterator = blogIterator;
+ }
+
+ @Override
+ public boolean hasContexts() {
+ return true;
+ }
+
+ @Override
+ public boolean hasPayloads() {
+ return true;
+ }
+
+ public Comparator<BytesRef> getComparator() {
+ return null;
+ }
+
+ @Override
+ public BytesRef next() {
+ if (blogIterator.hasNext()) {
+ currentBlog = blogIterator.next();
+ try {
+ //返回当前Project的name值,把blog类的name属性值作为key
+ return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 将Blog对象序列化存入payload
+ * 可以只将所需要的字段存入payload,这里对整个实体类进行序列化,方便以后需求,不建议采用这种方法
+ */
+ @Override
+ public BytesRef payload() {
+ try {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bos);
+ out.writeObject(currentBlog);
+ out.close();
+ BytesRef bytesRef = new BytesRef(bos.toByteArray());
+ return bytesRef;
+ } catch (IOException e) {
+ logger.error("", e);
+ return null;
+ }
+ }
+
+ /**
+ * 文章标题
+ */
+ @Override
+ public Set<BytesRef> contexts() {
+ try {
+ Set<BytesRef> regions = new HashSet<BytesRef>();
+ regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
+ return regions;
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Couldn't convert to UTF-8");
+ }
+ }
+
+ /**
+ * 返回权重值,这个值会影响排序
+ * 这里以产品的销售量作为权重值,weight值即最终返回的热词列表里每个热词的权重值
+ */
+ @Override
+ public long weight() {
+ return currentBlog.getHits(); //change to hits
+ }
+}
+
/**
+ * ajax建立索引
+ */
+@Override
+public void ajaxbuild() {
+ try {
+ Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+ SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+ AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+ //创建Blog测试数据
+ List<Blog> blogs = blogMapper.getAllBlog();
+ suggester.build(new BlogIterator(blogs.iterator()));
+ } catch (IOException e) {
+ System.err.println("Error!");
+ }
+}
+
@Override
+public Set<String> ajaxsearch(String keyword) {
+ try {
+ Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+ SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+ AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+ List<String> list = lookup(suggester, keyword);
+ list.sort(new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ if (o1.length() > o2.length()) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ });
+ Set<String> set = new LinkedHashSet<>();
+ for (String string : list) {
+ set.add(string);
+ }
+ ssubSet(set, 7);
+ return set;
+ } catch (IOException e) {
+ System.err.println("Error!");
+ return null;
+ }
+}
+
@RequestMapping("ajaxsearch")
+public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ String keyword = request.getParameter("keyword");
+ if (StringUtils.isEmpty(keyword)) {
+ return;
+ }
+ Set<String> set = blogService.ajaxsearch(keyword);
+ Gson gson = new Gson();
+ response.getWriter().write(gson.toJson(set));//返回的数据使用json
+}
+
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
+<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
+<script type="text/javascript">
+ /******************** remote start **********************/
+ $('#remote_input').autocomplete({
+ source: [
+ {
+ url: "ajaxsearch.html?keyword=%QUERY%",
+ type: 'remote'
+ }
+ ]
+ });
+ /********************* remote end ********************/
+</script>
+
参考:
https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/
http://iamyida.iteye.com/blog/2205114
先看一下Quartz的架构图:
<dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ <version>4.2.6.RELEASE</version>
+</dependency>
+
(2)导入quartz包
<dependency>
+ <groupId>org.quartz-scheduler</groupId>
+ <artifactId>quartz</artifactId>
+ <version>2.3.0</version>
+</dependency>
+
(3)mysql远程备份 使用命令行工具仅仅需要一行:
mysqldump -u [username] -p[password] -h [hostip] database > file
+
但是java不能直接执行linux的命令,仍旧需要依赖第三方库ganymed
<dependency>
+ <groupId>ch.ethz.ganymed</groupId>
+ <artifactId>ganymed-ssh2</artifactId>
+ <version>262</version>
+</dependency>
+
完整代码如下:
@Component("mysqlService")//在spring中注册一个mysqlService的Bean
+public class MysqlUtil {
+ ...
+ StringBuffer sb = new StringBuffer();
+ sb.append("mysqldump -u " + username + " -p" + password + " -h " + host + " " +
+ database + " >" + file);
+ String sql = sb.toString();
+ Connection connection = new Connection(s_host);
+ connection.connect();
+ boolean isAuth = connection.authenticateWithPassword(s_username, s_password);//进行远程服务器登陆认证
+ if (!isAuth) {
+ logger.error("server login error");
+ }
+ Session session = connection.openSession();
+ session.execCommand(sql);//执行linux语句
+ InputStream stdout = new StreamGobbler(session.getStdout());
+ BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
+ ...
+}
+
(4)spring中配置quartz
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
+ <property name="targetObject" ref="mysqlService"/>
+ <property name="targetMethod" value="exportDataBase"/>
+</bean>
+<!--定义触发时间 -->
+<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
+ <property name="jobDetail" ref="jobDetail"/>
+ <!-- cron表达式,每周五2点59分运行-->
+ <property name="cronExpression" value="0 59 2 ? * FRI"/>
+</bean>
+<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->
+<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
+ <property name="triggers">
+ <list>
+ <ref bean="myTrigger"/>
+ </list>
+ </property>
+</bean>
+
(5)java完整文件在这
java ee项目的定时任务中除了运行quartz之外,spring3+还提供了task,可以看做是一个轻量级的Quartz,而且使用起来比Quartz简单的多。
(1)spring配置文件中配置:
<task:annotation-driven/>
+
(2)最简单的例子,在所需要的函数上添加定时任务即可运行
@Scheduled(fixedRate = 5000)
+ public void reportCurrentTime() {
+ System.out.println("每隔5秒运行一次" + sdf.format(new Date()));
+ }
+
(3)运行的时候会报错:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
+ at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
+ at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
+ at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
+ at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:163)
+ at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:136)
+ at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:380)
+
参考:
1.http://blog.csdn.net/oarsman/article/details/52801877
2.http://stackoverflow.com/questions/31199888/spring-task-scheduler-no-qualifying-bean-of-type-org-springframework-scheduli
Spring的定时任务调度器会尝试获取一个注册过的 task scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的scheduler bean,获取的步骤如下:
1.尝试从配置中找到一个TaskScheduler Bean
2.寻找ScheduledExecutorService Bean
3.使用默认的scheduler 修改log4j.properties即可: log4j.logger.org.springframework.scheduling=INFO
其实这个功能不影响定时器的功能。 (4)结果:
每隔5秒运行一次14:44:34
+每隔5秒运行一次14:44:39
+每隔5秒运行一次14:44:44
+
欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使用ELK,但是,本网站的服务器配置实在太低了(1GHZ、2G内存),压根就跑不起ELK,所以只能寻求其他方式,目前最常用的有百度统计和友盟,这里,本人使用的是百度统计,提供了API给开发者使用,能够将自己所需要的图表移植到自己的网站上。日志是网站及其重要的文件,通过对日志进行统计、分析、综合,就能有效地掌握网站运行状况,发现和排除错误原因,了解客户访问分布等,更好的加强系统的维护和管理。下面是我的百度统计的概览页面:
企业级的网站日志不能公开,但是我的是个人网站,用来跟大家一起学习的,所以,需要将百度的统计页面展示出来,但是,百度并不提供日志的图像,只提供API给开发者调用,而且还限制访问次数,一天不能超过2000次,这个对于实时统计来说,确实不够,所以只能展示前几天的访问统计。这里的日志系统分为三个步骤:1.API获取数据;2.存储数据;3.展示数据。页面效果如下,也可以点开我的网站的日志系统:
百度统计提供了Tongji API的Java和Python版本,这两个版本及其复杂,可用性极低,所以,本人用Python写了个及其简单的通用版本,整体只有28行,代码在这,https://github.com/Zephery/baidutongji。下面是具体过程
先在百度统计中注册登录之后,进入管理页面,新增网站,然后在代码管理中获取安装代码,大部分人的代码都是类似的,除了hm.js?后面的参数,是记录该网站的唯一标识。
<script>
+var _hmt = _hmt || [];
+(function() {
+ var hm = document.createElement("script");
+ hm.src = "https://hm.baidu.com/hm.js?code";
+ var s = document.getElementsByTagName("script")[0];
+ s.parentNode.insertBefore(hm, s);
+})();
+</script>
+
同时,需要在申请其他设置->数据导出服务中开通数据导出服务,百度统计Tongji API可以为网站接入者提供便捷的获取网站流量数据的通道。
至此,我们获得了username、password、token,然后开始使用三个参数来获取数据。
官网的API详细的记录了接口的参数以及解释, 链接:https://api.baidu.com/json/tongji/v1/ReportService/getData,详细的官方报告请访问官网TongjiApi 所需参数(必须):
参数名称 | 参数类型 | 描述 |
---|---|---|
method | string | 要查询的报告 |
start_date | string | 查询起始时间 |
end_date | string | 查询结束时间 |
metrics | string | 自定义指标 |
其中,参数start_date和end_date的规定为:yyyyMMdd,这里我们使用python的原生库,datetime、time,获取昨天的时间以及前七天的日期。
today = datetime.date.today() # 获取今天的日期
+yesterday = today - datetime.timedelta(days=1) # 获取昨天的日期
+fifteenago = today - datetime.timedelta(days=7) # 获取前七天的日期
+end, start = str(yesterday).replace("-", ""), str(fifteenago).replace("-", "") # 格式化成yyyyMMdd格式
+
说明:siteId可以根据个人百度统计的链接获取,也可以使用Tongji API的第一个接口列表获取用户的站点列表。首先,我们构建一个类,由于username、password、token都是通用的,所以我们将它设置为构造方法的参数。
class Baidu(object):
+ def __init__(self, siteId, username, password, token):
+ self.siteId = siteId
+ self.username = username
+ self.password = password
+ self.token = token
+
然后构建一个共同的方法,用来获取提交数据之后返回的结果,其中提供了4个可变参数,分别是(start_date:起始日期,end_date:结束日期,method:方法,metrics:指标),返回的是字节,最后需要decode("utf-8")一下变成字符:
def getresult(self, start_date, end_date, method, metrics):
+ body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+ "username": self.username},
+ "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+ "end_date": end_date,
+ "metrics": metrics}}
+ data = bytes(json.dumps(body), 'utf8')
+ req = urllib.request.Request(base_url, data)
+ response = urllib.request.urlopen(req)
+ the_page = response.read()
+ return the_page.decode("utf-8")
+
至此,python获取百度统计的过程基本就没了,没错,就是那么简简单单的几行,完整代码见https://github.com/Zephery/baidutongji/blob/master/baidu.py,但是,想要实现获取各种数据,仍需要做很多工作。
(1)需要使用其他参数怎么办
python中提供了个可变参数来解决这一烦恼,详细请看http://www.jianshu.com/p/98f7e34845b5,可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple,而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
def getresult(self, start_date, end_date, method, metrics, **kw):
+ base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+ body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+ "username": self.username},
+ "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+ "end_date": end_date, "metrics": metrics}}
+ for key in kw: #对可变参数进行遍历,如果有的话就往body中加入
+ body['body'][key] = kw[key]
+
使用方式:
result = self.getresult(start, end, "source/all/a",
+ "pv_count,visitor_count,avg_visit_time", viewType='visitor') #其中viewTYpe便是可变参数
+
(2)获取的数据如何解析 百度统计返回的结果比较简洁而又反人类,以获取概览中的pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time为例子:
result = bd.getresult(start, end, "overview/getTimeTrendRpt",
+ "pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time")
+
返回的结果是:
[[['2017/09/12'], ['2017/09/13'], ['2017/09/14'], ['2017/09/15'], ['2017/09/16'], ['2017/09/17'], ['2017/09/18']],
+[[422, 76, 76, 41.94, 221],
+ [284, 67, 65, 50.63, 215],
+ [67, 23, 22, 52.17, 153],
+ [104, 13, 13, 36.36, 243],
+ [13, 4, 4, 33.33, 66],
+ [73, 7, 6, 37.5, 652],
+ [63, 11, 11, 33.33, 385]
+ ], [], []]
+
即:翻译成人话就是:
[[[date1,date2,...]],
+ [[date1的pv_count, date1的visitor_count, date1的ip_count, date1的bounce_ratio, date1的avg_visit_time],
+ [date2的pv_count, date2的visitor_count, date2的ip_count, date2的bounce_ratio, date2的avg_visit_time],
+ ...,[]
+ ],[],[]]
+
极其反人类的设计。还好用的python,python数组的特性实在太强了。出了可以运用[x for x in range]这类语法之外,还能与三元符(x if y else x+1,如果y成立,那么结果是x,如果y不成立,那么结果是x+1)一起使用,这里注意:如果当天访问量为0,其返回的json结果是'--',所以要判断是不是为'--',归0化,才能在折线图等各种图上显示。下面是pv_count的例子:
pv_count = [x[0] if x[0] != '--' else 0 for x in result[1]]
+
(3)每周限制2000次 在开通数据导出服务的时候,不知道大家有没有注意到它的说明,即我们是不能实时监控的,只能将它放在临时数据库中,这里我们选择了Redis,并在centos里定义一个定时任务,每天全部更新一次即可。
python中redis的使用方法很简单,连接跟mysql类似:
# 字符串
+pool = redis.ConnectionPool(host='your host ip', port=port, password='your auth') # TODO redis地址
+r = redis.Redis(connection_pool=pool)
+
本网站使用redis的数据结构只有set,方法也很简单,就是定义一个key,然后value是数组的字符串获取json。
ip_count = [x[2] if x[2] != '--' else 0 for x in result[1]]
+r.set("ip_count", ip_count)
+# json
+name = [item[0]['name'] for item in data[0]]
+count = 0
+tojson = []
+for item in data[1]:
+ temp = {}
+ temp["name"] = name[count]
+ temp["pv_count"] = item[0]
+ temp["visitor_count"] = item[1]
+ temp["average_stay_time"] = item[2]
+ tojson.append(temp)
+ count = count + 1
+r.set("rukouyemian", json.dumps(tojson[:5]))
+
下面是基本的使用代码,完整的使用代码就不贴了,有兴趣可以去我的github上看看,完整代码,希望能给个star哈哈哈,感谢
import json
+import time
+import datetime
+import urllib.parse
+import urllib.request
+
+base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+
+class Baidu(object):
+ def __init__(self, siteId, username, password, token):
+ self.siteId = siteId
+ self.username = username
+ self.password = password
+ self.token = token
+
+ def getresult(self, start_date, end_date, method, metrics, **kw):
+ base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+ body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+ "username": self.username},
+ "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+ "end_date": end_date, "metrics": metrics}}
+ for key in kw:
+ body['body'][key] = kw[key]
+ data = bytes(json.dumps(body), 'utf8')
+ req = urllib.request.Request(base_url, data)
+ response = urllib.request.urlopen(req)
+ the_page = response.read()
+ return the_page.decode("utf-8")
+
+if __name__ == '__main__':
+ # 日期开始
+ today = datetime.date.today()
+ yesterday = today - datetime.timedelta(days=1)
+ fifteenago = today - datetime.timedelta(days=7)
+ end, start = str(yesterday).replace("-", ""), str(fifteenago).replace("-", "")
+ # 日期结束
+ bd = Baidu(yoursiteid, "username", "password", "token")
+ result = bd.getresult(start, end, "overview/getTimeTrendRpt",
+ "pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time")
+ result = json.loads(result)
+ base = result["body"]["data"][0]["result"]["items"]
+ print(base)
+
+
在将数据存进redis中之后,我们需要在博客中使用这些数据来制作图表。在newblog中使用方式也很简单,大概就是使用jedis读取数据,然后使用echarts或者highcharts展示。其中折线图以及线型图我都使用了highcharts,确实比echarts好看的多,但是地域图还是选择了echarts,毕竟中国的产品还是对中国的支持较好。 (1)PV、UV折线图 以图表PV、UV为例,由于存储进redis的是一个数组,所以,可以直接从redis中读取然后放到一个attribute里即可:
String pv_count = jedis.get("pv_count");
+String visitor_count = jedis.get("visitor_count");
+mv.addObject("pv_count", pv_count);
+mv.addObject("visitor_count", visitor_count);
+
jsp中的使用如下:
<div class="panel-heading" style="background-color: rgba(187,255,255,0.7)">
+ <div class="card-title">
+ <strong>PV和UV折线图</strong>
+
+
+ <div class="panel-body">
+ <div id="linecontainer" style="width: auto;height: 330px">
+ <script>
+ var chart = new Highcharts.Chart('linecontainer', {
+ title: {
+ text: null
+ },
+ credits: {
+ enabled: false
+ },
+ xAxis: {
+ categories: ${daterange}
+ },
+ yAxis: {
+ title: {
+ text: '次数'
+ },
+ plotLines: [{
+ value: 0,
+ width: 1,
+ color: '#808080'
+ }]
+ },
+ tooltip: {
+ valueSuffix: '次'
+ },
+ legend: {
+ borderWidth: 0,
+ align: "center", //程度标的目标地位
+ verticalAlign: "top", //垂直标的目标地位
+ x: 0, //间隔x轴的间隔
+ y: 0 //间隔Y轴的间隔
+ },
+ series: [{
+ name: 'pv',
+ data:${pv_count}
+ }, {
+ name: 'uv',
+ data:${visitor_count}
+ }]
+ })
+ </script>
+
效果如下:
(2)地域访问量 在python代码中先获取地域的数据,其结果如下,百度统计跟echarts都是百度的,果然,自家人对自己人的支持真是特别友好的。
[{'pv_count': 649, 'pv_ratio': 7, 'visitor_count': 2684, 'name': '广东'}, {'pv_count': 2, 'pv_ratio': 2, 'visitor_count': 76, 'name': '四川'}, {'pv_count': 1, 'pv_ratio': 1, 'visitor_count': 3, 'name': '江苏'}]
+
地域图目前支持最好的还是百度的echarts,使用方法见echarts的官网吧,这里不再阐述,展示地域图的时候需要获取下载两个文件,china.js(其提供了js和json,这里使用的js),echarts.js。 部分代码:
<script type="text/javascript">
+ var myChart = echarts.init(document.getElementById('diyu'));
+ option = {
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ visualMap: {
+ min: 0,
+ max:${diyumax},
+ left: 'left',
+ top: 'bottom',
+ text: ['高', '低'], // 文本,默认为数值文本
+ calculable: true
+ },
+ toolbox: {
+ show: true,
+ orient: 'vertical',
+ left: 'right',
+ top: 'center',
+ feature: {
+ dataView: {readOnly: false},
+ restore: {},
+ saveAsImage: {}
+ }
+ },
+ series: [
+ {
+ name: '访问量',
+ type: 'map',
+ mapType: 'china',
+ roam: false,
+ label: {
+ normal: {
+ show: true
+ },
+ emphasis: {
+ show: true
+ }
+ },
+ data: [
+ <c:forEach var="diyu" items="${diyu}">
+ {name: '${diyu.name}', value: ${to.pv_count}},
+ </c:forEach>
+ ]
+ }
+ ]
+ };
+ myChart.setOption(option);
+</script>
+
结果如下:
网上关于日志系统的几乎都是ELK,对于小网站的,隐私不是很重要的还是可以用用百度统计的,这套系统也折磨了我挺久的,特别是它那反人类的返回数据。期初本来是想使用百度统计的,后来考虑了一下ELK,尝试之后发现,服务器配置跑不起来,还是安安稳稳的使用了百度统计,于此做成了这个系统,美观度还是不高,颜色需要优化一下。最后,希望能在GitHub上给我个star吧。
日志系统地址:http://www.wenzhihuai.com/log.html
个人网站网址:http://www.wenzhihuai.com
个人网站代码地址:https://github.com/Zephery/newblog
百度统计python代码地址:https://github.com/Zephery/baidutongji
万分感谢
欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。
洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。
一般情况下,当单实例无法支撑起用户的请求时,就需要就行扩容,部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区,比如:深圳的用户却访问到了北京的节点,然后还得从北京返回处理之后的数据,光是来回就至少得30ms。这部分可以通过智能DNS(就近访问)解决。而分机房,需要将请求合理的分配到不同的服务器,这部分就是我们所需要处理的。 通常,负载均衡分为硬件和软件两种,硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层,LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡,但是暂不讨论(我木有硬件条件)。下图来自张开涛的《亿级流量网站架构核心技术》
本站并没有那么多的服务器,目前只有两台,搭建不了那么大型的架构,就简陋的用两台服务器来模拟一下负载均衡的搭建。下图是本站的简单架构:
其中服务器A(119.23.46.71)为深圳节点,服务器B(47.95.10.139)为北京节点,搭建Nginx之后流量是这么走的:user->A->B-A->user或者user->A->user,第一条中A将请求转发给B,然后B返回的是其运行结果的静态资源。因为这里仅仅是用来学习,所以请不要考虑因为地域导致延时的问题。。。。下面是过程。
可以选择tar.gz、yum、rpm安装等,这里,由于编译、nginx配置比较复杂,要是没有把握还是使用rpm来安装吧,比较简单。从https://pkgs.org/download/nginx可以找到最新的rpm包,然后rpm -ivh 文件,然后在命令行中输入nginx即可启动,可以使用netstat检查一下端口。
启动后页面如下:
记一下常用命令
启动nginx,由于是采用rpm方式,所以环境变量什么的都配置好了。
+[root@beijingali ~]# nginx #启动nginx
+[root@beijingali ~]# nginx -s reload #重启nginx
+[root@beijingali ~]# nginx -t #校验nginx配置文件
+nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
+nginx: configuration file /etc/nginx/nginx.conf test is successful
+
Nginx常用的算法有: (1)round-robin:轮询,nginx默认的算法,从词语上可以看出,轮流访问服务器,也可以通过weight来控制访问次数。 (2)ip_hash:根据访客的ip,一个ip地址对应一个服务器。 (3)hash算法:hash算法常用的方式有根据uri、动态指定的consistent_key两种。 使用hash算法的缺点是当添加服务器的时候,只有少部分的uri能够被重新分配到新的服务器。这里,本站使用的是hash uri的算法,将不同的uri分配到不同的服务器,但是由于是不同的服务器,tomcat中的session是不一致,解决办法是tomcat session的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。
http{
+ ...
+ upstream backend {
+ hash $uri;
+ # 北京节点
+ server 47.95.10.139:8080;
+ # 深圳节点
+ server 119.23.46.71:8080;
+ }
+
+ server {
+ ...
+ location / {
+ root html;
+ index index.html index.htm;
+ proxy_pass http://backend;
+ ...
+ }
+ ...
+
之前有使用过ELK来跟踪日志,所以将日志格式化成了json的格式,这里贴一下吧
...
+ log_format main '{"@timestamp":"$time_iso8601",'
+ '"host":"$server_addr",'
+ '"clientip":"$remote_addr",'
+ '"size":$body_bytes_sent,'
+ '"responsetime":$request_time,'
+ '"upstreamtime":"$upstream_response_time",'
+ '"upstreamhost":"$upstream_addr",'
+ '"http_host":"$host",'
+ '"url":"$uri",'
+ '"xff":"$http_x_forwarded_for",'
+ '"referer":"$http_referer",'
+ '"agent":"$http_user_agent",'
+ '"status":"$status"}';
+ access_log logs/access.log main;
+ ...
+
配置完上流服务器之后,需要配置Http的代理,将请求的端口转发到proxy_pass设定的上流服务器,即当我们访问http://wwww.wenzhihuai.com的时候,请求会被转发到backend中配置的服务器,此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是,仔细注意之后,我们会发现,tomcat中的访问日志ip来源都是127.0.0.1,相当于本地访问自己的资源。由于后台中有处理ip的代码,对客户端的ip、访问uri等记录下来,所以需要设置nginx来获取用户的实际ip,参考nginx 配置。参考文中的一句话:经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,通过$remote_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的,也就是说nginx使用$remote_addr变量时获得的是用户的真实ip,如果我们想要在web端获得用户的真实ip,就必须在nginx这里作一个赋值操作,如下:
location / {
+ root html;
+ index index.html index.htm;
+ proxy_pass http://backend;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_set_header REMOTE-HOST $remote_addr;
+ }
+
(1)proxy_set_header X-real-ip $remote_addr;
其中这个X-real-ip是一个自定义的变量名,名字可以随意取,这样做完之后,用户的真实ip就被放在X-real-ip这个变量里了,然后,在web端可以这样获取: request.getAttribute("X-real-ip")
(2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
X-Forwarded-For:squid开发的,用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准,这个不是默认有的,其经过代理转发之后,格式为client1, proxy1, proxy2,如果想通过这个变量来获取用户的ip,那么需要和$proxy_add_x_forwarded_for一起使用。
$proxy_add_x_forwarded_for:现在的$proxy_add_x_forwarded_for变量,X-Forwarded-For部分包含的是用户的真实ip,$remote_addr部分的值是上一台nginx的ip地址,于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip,第一台nginx的ip”。
HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。一般情况下,能通过服务器的ssh来生成ssl证书,但是如果使用是自己的,一般浏览器(谷歌、360等)都会报证书不安全的错误,正常用户都不敢访问吧==,所以现在使用的是腾讯跟别的机构颁发的:
首先需要下载证书,放在nginx.conf相同目录下,nginx上的配置也需要有所改变,在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥:
ssl_certificate 1_www.wenzhihuai.com_bundle.crt; #主要文件路径
+ ssl_certificate_key 2_www.wenzhihuai.com.key;
+ ssl_session_timeout 5m; # 超时时间
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
+ ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
+ ssl_prefer_server_ciphers on;
+
至此,可以使用https来访问了。https带来的安全性(保证信息安全、识别钓鱼网站等)是http远远不能比拟的,目前大部分网站都是实现全站https,还能将http自动重定向为https,此处,需要在server中添加rewrite ^(.*) https://$server_name$1 permanent;即可
配置好了负载均衡之后,如果有一台服务器挂了怎么办?nginx中提供了可配置的服务器存活的识别,主要是通过max_fails失败请求次数,fail_timeout超时时间,weight为权重,下面的配置的意思是当服务器超时10秒,并失败了两次的时候,nginx将认为上游服务器不可用,将会摘掉上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试
upstream backend_server {
+ server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;
+ server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;
+}
+
分布式情况下难免会要解决session共享的问题,目前推荐的方法基本上都是使用redis,网上查找的方法目前流行的有下面四种,参考自tomcat 集群中 session 共: 1.使用 filter 方法存储。(推荐,因为它的服务器使用范围比较多,不仅限于tomcat ,而且实现的原理比较简单容易控制。) 2.使用 tomcat sessionmanager 方法存储。(直接配置即可) 3.使用 terracotta 服务器共享。(不知道,不了解) 4.使用spring-session。(spring的一个小项目,其原理也和第一种基本一致)
本站使用spring-session,毕竟是spring下的子项目,学习下还是挺好的。参考Spring-Session官网。官方文档提供了spring-boot、spring等例子,可以参考参考。目前最新版本是2.0.0,不同版本使用方式不同,建议看官网的文档吧。
首先,添加相关依赖
<dependency>
+ <groupId>org.springframework.session</groupId>
+ <artifactId>spring-session-data-redis</artifactId>
+ <version>1.3.1.RELEASE</version>
+ <type>pom</type>
+ </dependency>
+ <dependency>
+ <groupId>redis.clients</groupId>
+ <artifactId>jedis</artifactId>
+ <version>${jedis.version}</version>
+ </dependency>
+
新建一个session.xml,然后在spring的配置文件中添加该文件,然后在session.xml中添加:
<!-- redis -->
+ <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
+ </bean>
+
+ <bean id="jedisConnectionFactory"
+ class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+ <property name="hostName" value="${host}" />
+ <property name="port" value="${port}" />
+ <property name="password" value="${password}" />
+ <property name="timeout" value="${timeout}" />
+ <property name="poolConfig" ref="jedisPoolConfig" />
+ <property name="usePool" value="true" />
+ </bean>
+
+ <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+ <property name="connectionFactory" ref="jedisConnectionFactory" />
+ </bean>
+
+ <!-- 将session放入redis -->
+ <bean id="redisHttpSessionConfiguration"
+ class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
+ <property name="maxInactiveIntervalInSeconds" value="1800" />
+ </bean>
+
然后我们需要保证servlet容器(tomcat)针对每一个请求都使用springSessionRepositoryFilter来拦截
<filter>
+ <filter-name>springSessionRepositoryFilter</filter-name>
+ <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+</filter>
+<filter-mapping>
+ <filter-name>springSessionRepositoryFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ <dispatcher>REQUEST</dispatcher>
+ <dispatcher>ERROR</dispatcher>
+</filter-mapping>
+
配置完成,使用RedisDesktopManager查看结果:
访问http://www.wenzhihuai.com
tail -f localhost_access_log.2017-11-05.txt查看日志,然后清空一下当前记录
访问技术杂谈页面,此时nginx将请求转发到119.23.46.71服务器,session为28424f91-5bc5-4bba-99ec-f725401d7318。
点击生活笔记页面,转发到的服务器为47.95.10.139,session为28424f91-5bc5-4bba-99ec-f725401d7318,与上面相同。session已保持一致。
值得注意的是:同一个浏览器,在没有关闭的情况下,即使通过域名访问和ip访问得到的session是不同的。 欢迎访问我的个人网站O(∩_∩)O哈哈~希望能给个star 个人网站网址:http://www.wenzhihuai.com
个人网站代码地址:https://github.com/Zephery/newblog
先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:
由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:
果然,36ms。。。看起来挺小的,但是对比一下sql执行语句的时间:
大部分都能在10ms内完成,而最长的语句是insert语句,可见,由于异地导致的36ms延时还是比较大的,捣鼓了一下,最后还是选择换个架构,每个服务器读取自己的数据库,然后数据库底层做一下主主复制,让数据同步。最终架构如下:
数据库复制的基本问题就是让一台服务器的数据与其他服务器保持同步。MySql目前支持两种复制方式:基于行的复制和基于语句的复制,这两者的基本过程都是在主库上记录二进制的日志、在备库上重放日志的方式来实现异步的数据复制。其过程分为三步:
(1)master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
(2)slave将master的binary log events拷贝到它的中继日志(relay log);
(3)slave重做中继日志中的事件,将改变反映它自己的数据。
该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。 下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。 SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。 此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。 MySql的基本复制方式有主从复制、主主复制,主主复制即把主从复制的配置倒过来再配置一遍即可,下面的配置则是主从复制的过程,到时候可自行改为主主复制。其他的架构如:一主库多备库、环形复制、树或者金字塔型都是基于这两种方式,可参考《高性能MySql》。
由于是个自己的小网站,就不做过多的操作了,直接使用root账号
接下来要对mysql的serverID,日志位置,复制方式等进行操作,使用vim打开my.cnf。
[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# master
+log-bin=mysql-bin
+# 设为基于行的复制
+binlog-format=ROW
+# 设置server的唯一id
+server-id=2
+# 忽略的数据库,不使用备份
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 要进行备份的数据库
+binlog-do-db=myblog
+
重启Mysql之后,查看主库状态,show master status。
其中,File为日志文件,指定Slave从哪个日志文件开始读复制数据,Position为偏移,从哪个POSITION号开始读,Binlog_Do_DB为要备份的数据库。
从库的配置跟主库类似,vim /etc/my.cnf配置从库信息。
+[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# slave
+log-bin=mysql-bin
+# 服务器唯一id
+server-id=3
+# 不备份的数据库
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 需要备份的数据库
+replicate-do-db=myblog
+# 其他相关信息
+slave-skip-errors=all
+slave-net-timeout=60
+# 开启中继日志
+relay_log = mysql-relay-bin
+#
+log_slave_updates = 1
+# 防止改变数据
+read_only = 1
+
重启slave,同时启动复制,还需要调整一下命令。
mysql> CHANGE MASTER TO MASTER_HOST = '119.23.46.71', MASTER_USER = 'root', MASTER_PASSWORD = 'helloroot', MASTER_PORT = 3306, MASTER_LOG_FILE = 'mysql-bin.000009', MASTER_LOG_POS = 346180;
+
+
可以看见slave已经开始进行同步了。我们使用show slave status\G来查看slave的状态。
其中日志文件和POSITION不一致是合理的,配置好了的话,即使重启,也不会影响到主从复制的配置。
某天在Github上漂游,发现了阿里的canal,同时才知道上面这个业务是叫异地跨机房同步,早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。下面是基本的原理:
原理相对比较简单:
1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
2.mysql master收到dump请求,开始推送binary log给slave(也就是canal)
3.canal解析binary log对象(原始为byte流)
其中,配置过程如下:https://github.com/alibaba/canal,可以搭配Zookeeper使用。在ZKUI中能够查看到节点:
一般情况下,还要配合阿里的另一个开源产品使用otter,相关文档还是找找GitHub吧,个人搭建完了之后,用起来还是不如直接使用mysql的主主复制,而且异地机房同步这种大企业才有的业务。
公司又要996了,实在是忙不过来,感觉自己写的还是急躁了点,困==
欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
本站主要用到的插件有:
1.wowslider
2.畅言
3.Editor.md
4.highchart、echart 5.百度分享
6.waterfall.js
7.心知天气
8.标签云
可能是我这网站中最炫的东西了,图片能够自动像幻灯片一样自动滚动,让网站的首页一看起来就高大上,简直就是建站必备的东西,而且安装也及其简单,有兴趣可以点击官网看看。GitHub里也开放了源代码。安装过程:自己选择“幻灯片”切换效果,保存为html就行了,WORDPREESS中好像有集成这个插件的,做的还更好。感兴趣可以点击我的博客首页看一看。
不过还有个值得注意的问题,就是wowslider里面带有一个googleapis的服务,即https://fonts.googleapis.com/css?family=Arimo&subset=latin,cyrillic,latin-ext,由于一般用户不能访问谷歌,会导致网页加载速度及其缓慢,所以,去掉为妙
作为社交评论的工具,虽然说表示还是想念以前的多说,但是畅言现在做得还是好了,有评论审核,评论导出导入等功能,如果浏览量大的话,还能提供广告服务,让站长也能拿到一丢丢的广告费。本博客中使用了畅言的基本评论、获取某篇文章评论数的功能。可以到我这里留言哈
一款能将markdown解析为html的插件,国人的作品,博客的文章编辑器一开始想使用的是markdown,想法是:写文章、保存数据库都是markdown格式的,保存在数据库中,读取时有需要解析markdown,这个过程是有点耗时的,但是相比把html式的网页保存在数据库中友好点吧,因为如果一篇文章比较长的话,转成html的格式,光是大小估计也得超过几十kb?所以,还是本人选择的是一切都用源markdown。 editor.md,是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。页面看起来还是美观的,相比WORDPRESS的那些牛逼插件还差那么一点点,不过从普通人的眼光来看,应该是可以的了。此处,我用它作为解析网页的利器,还有就是后台编辑也是用的这个。
代码样式,这一点是不如WORDPRESS的插件了,不过已经可以了。
目前最常用的是highcharts跟echart,目前个人博客中的日志系统主要还是采用了highcharts,主要还是颜色什么的格调比较相符吧,其次是因为对echarts印象不太友好,比如下面做这张,打开网页后,缩小浏览器,百度的地域图却不能自适应,出现了越界,而highcharts的全部都能自适应调整。想起有次面试,我说我用的highcharts,面试官一脸嫌弃。。。(网上这么多人鄙视百度是假的?)
不过地图确确实实是echarts的优势,毕竟还是自家的东西了解自家,不过前段时间去看了看echarts的官网,已经不提供下载了。如果有需要,还是去csdn上搜索吧,或者替换为highmap。
作为一个以博客为主的网站,免不了使用一些社会化分享的工具,目前主要是jiathis和百度分享,这两者的ui都是相似的(丑爆了)。凭我个人的感觉,jiathis加载实在是太过于缓慢,这点是无法让人忍受的,只好投靠百度。百度分享类似于jiathis,安装也很简单,具体见官网http://share.baidu.com/code/advance#tools。一直点点点,配置完之后,就是下图这种,丑爆了是不是?
好在对它的美观改变不是很难,此处参考了别人的UI设计,原作者我忘记怎么联系他了。其原理主要是使用图片来替换掉原本的东西。完整的源码可以点击此处。
#share a {
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ margin: 6px;
+ border-radius: 25px;
+ transition: all .4s;
+}
+/*主要的是替换掉backgound的背景图片*/
+#share a.bds_qzone {
+ background: url(http://image.wenzhihuai.com/t_QQZone.png) no-repeat;
+ background-size: 34px 34px;
+}
+
改完之后的效果。
有段时间,瀑布流特别流行?还有段时间,瀑布流开始遭到各种抵制。。。看看知乎的人怎么说,大部分人不喜欢的原因是让人觉得视觉疲劳,不过瀑布流最大的好处还是有的:提高发现好图的效率以及图片列表页极强的视觉感染力。没错,我还是想把自己的网站弄得铉一些==,所以采用了瀑布流(不过效果不是很好,某些浏览器甚至加载出错),这个大bug有时间再改,毕竟花了很多时间做的这个,效果确实不咋地。目前主要的瀑布流有waterfall.js和masory.js。这一块目前还不是很完善,希望能得到各位大佬的指点。
此类咨询服务还是网上还是挺多的,这一块不知道是不是所谓的“画蛇添足”这部分,主要是我觉得网站右边这部分老是少了点什么,所以加上了天气插件。目前常用的天气插件有中国天气网,心知天气等。安装方式见各自的官网,这里不再阐述,我使用的是心知天气。注意:心知天气限制流量的,一个小时内只能使用400次,如果超出了会失效,当然也可以付费使用。
标签云,弄得好的话应该说是一个网站的点缀。现在好像比较流行3D的标签云?像下面这种。
从个人的网站风格来看,比较适应PHP形式的,有点颜色而又不绚丽的即可,之前用的跟分类的一样的样式,即双纵列的样式,美观度还行,虽然老是感觉有点怪怪的,如果某个标签的字数过长怎么办,岂不是要顶出div了。所以还是选择换另一种风格,最终偶然一次找到了下面这种,能够自适应宽度,颜色虽然鲜艳了点(以后有空再调一下吧),源码见style.css。 下图为目前的标签页。
作为一个后端人员,调css和js真是痛苦==,好在坚持下来了,虽然还是很多不足,以后有时间慢慢改。说了那么多,感觉自己还是菜的抠脚。 题外话,搭建一个博客,对于一个新近程序员来说真的是锻炼自己的一个好机会,能够认识到从前端、java后台、linux、jvm等等知识,只是真的有点耗时间(还不如把时间耗在Spring源码),如果不采用别人的框架的话,付出的代价还是蛮大的(所以不要鄙视我啦)。没有什么能够一举两得,看自己的取舍吧。加油💪(ง •_•)ง
欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。
系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 缓存常用语: 数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透 可查看Redis实战(一) 使用缓存合理性
从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。
步骤: (1)用户发送一个请求到nginx,nginx对请求进行分发。 (2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。 (3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。 (4)查询数据库的时候,mysql作了主主备份。
Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。
我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap来进行缓存。
(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。 (3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下: a.为每一个Mapper分配一个Cache缓存对象; b.多个Mapper共用一个Cache缓存对象;
在mybatis的配置文件中添加:
<settings>
+ <!--开启二级缓存-->
+ <setting name="cacheEnabled" value="true"/>
+</settings>
+
然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):
<cache eviction="LRU"
+ type="org.apache.ibatis.cache.impl.PerpetualCache"
+ flushInterval="120000"
+ size="1024"
+ readOnly="true"/>
+
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:
MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):
(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。 (3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。
下面将介绍使用Redis集中式缓存在个人网站的应用。
Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 **SpEL(Spring Expression Language)**来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。 下面是Spring Cache常用的注解:
(1)@Cacheable @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
(2)@CachePut @CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
(3)@CacheEvict @CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
属性 | 介绍 | 例子 |
---|---|---|
value | 缓存的名称,必选 | @Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
key | 缓存的key,可选,需要按照SpEL表达式填写 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,key=”#userName”) |
allEntries | 是否清空所有缓存内容,默认为false | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false | @CachEvict(value=”testcache”,beforeInvocation=true) |
但是有个问题: Spring官方认为:缓存过期时间由各个产商决定,所以并不提供缓存过期时间的注解。所以,如果想实现各个元素过期时间不同,就需要自己重写一下Spring cache。
一般是Spring常用的包+Spring data redis的包,记得注意去掉所有冲突的包,之前才过坑,Spring-data-MongoDB已经有SpEL的库了,和自己新引进去的冲突,搞得我以为自己是配置配错了,真是个坑,注意,开发过程中一定要去除掉所有冲突的包!!!
需要启用缓存的注解开关,并配置好Redis。序列化方式也要带上,否则会碰到幽灵bug。
<!-- 启用缓存注解开关,此处可自定义keyGenerator -->
+ <cache:annotation-driven/>
+ <bean id="jedisConnectionFactory"
+ class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+ <property name="hostName" value="${host}"/>
+ <property name="port" value="${port}"/>
+ <property name="password" value="${password}"/>
+ <property name="database" value="${redis.default.db}"/>
+ <property name="timeout" value="${timeout}"/>
+ <property name="poolConfig" ref="jedisPoolConfig"/>
+ <property name="usePool" value="true"/>
+ </bean>
+ <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+ <property name="connectionFactory" ref="jedisConnectionFactory"/>
+ <!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->
+ <property name="keySerializer">
+ <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
+ </property>
+ <property name="hashKeySerializer">
+ <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
+ </property>
+ <property name="valueSerializer">
+ <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
+ </property>
+ <property name="hashValueSerializer">
+ <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
+ </property>
+ </bean>
+ <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
+ <constructor-arg name="redisOperations" ref="redisTemplate" />
+ <!--统一过期时间-->
+ <property name="defaultExpiration" value="${redis.defaultExpiration}"/>
+ </bean>
+
在分布式系统中,很容易存在不同类相同名字的方法,如A.getAll(),B.getAll(),默认的key(getAll)都是一样的,会很容易产生问题,所以,需要自定义key来实现分布式环境下的不同。
@Component("customKeyGenerator")
+public class CustomKeyGenerator implements KeyGenerator {
+ @Override
+ public Object generate(Object o, Method method, Object... objects) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(o.getClass().getName());
+ sb.append(".");
+ sb.append(method.getName());
+ for (Object obj : objects) {
+ sb.append(obj.toString());
+ }
+ return sb.toString();
+ }
+}
+
之后,存储的key就变为:com.myblog.service.impl.BlogServiceImpl.getBanner。
在所需要的方法上添加注解,比如,首页中的那几张幻灯片,每次进入首页都需要查询数据库,这里,我们直接放入缓存里,减少数据库的压力,还有就是那些热门文章,访问量比较大的,也放进数据库里。
@Override
+ @Cacheable(value = "getBanner", keyGenerator = "customKeyGenerator")
+ public List<Blog> getBanner() {
+ return blogMapper.getBanner();
+ }
+ @Override
+ @Cacheable(value = "getBlogDetail", key = "'blogid'.concat(#blogid)")
+ public Blog getBlogDetail(Integer blogid) {
+ Blog blog = blogMapper.selectByPrimaryKey(blogid);
+ if (blog == null) {
+ return null;
+ }
+ Category category = categoryMapper.selectByPrimaryKey(blog.getCategoryid());
+ blog.setCategory(category);
+ List<Tag> tags = tagMapper.getTagByBlogId(blog.getBlogid());
+ blog.setTags(tags.size() > 0 ? tags : null);
+ asyncService.updatebloghits(blogid);//异步更新阅读次数
+ logger.info("没有走缓存");
+ return blog;
+ }
+
我们调用一个getBlogDetail(获取博客详情)100次来对比一下时间。连接的数据库在深圳,本人在广州,还是有那么一丢丢的网路延时的。
public class SpringTest {
+ @Test
+ public void init() {
+ ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:spring-test.xml");
+ IBlogService blogService = (IBlogService) ctx.getBean("blogService");
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < 100; i++) {
+ blogService.getBlogDetail(615);
+ }
+ System.out.println(System.currentTimeMillis() - startTime);
+ }
+}
+
为了做一下对比,我们同时使用mybatis自身缓存来进行测试。
统计出结果如下:
没有使用任何缓存(mybatis一级缓存没有关闭):18305
+使用远程Redis缓存:12727
+使用Mybatis缓存:6649
+使用本地Redis缓存:5818
+
由结果看出,缓存的使用大大较少了获取数据的时间。
部署进个人博客之后,redis已经缓存的数据:
个人网站中共有两个栏目,一个是技术杂谈,另一个是生活笔记,每点击一次栏目的时候,会根据页数从数据库中查询数据,百度了下,大概有三种方法: (1)以页码作为Key,然后缓存整个页面。 (2)分条存取,只从数据库中获取分页的文章ID序列,然后从service(缓存策略在service中实现)中获取。 第一种,由于使用了第三方的插件PageHelper,分页获取的话会比较麻烦,同时整页缓存对内存压力也蛮大的,毕竟服务器只有2g。第二条实现方式简单,缺陷是依旧需要查询数据库,想了想还是放弃了。缓存的初衷是对请求频繁又不易变的数据,实际使用中很少会反复的请求同一页的数据(查询条件也相同),当然对数据中某些字段做缓存还是有必要的。
对于文章来说,内容是不经常更新的,没有涉及到缓存一致性,但是对于文章的阅读量,用户每点击一次,就应该更新浏览量的。对于文章的缓存,常规的设计是将文章存储进数据库中,然后读取的时候放入缓存中,然后将浏览量以文章ID+浏览量的结构实时的存入redis服务器中。本站当初设计不合理,直接将浏览量作为一个字段,用户每点击一次的时候就异步更新浏览量,但是此处没有更新缓存,如果手动更新缓存的话,基本上每点击一次都得执行更新操作,同样也不合理。所以,目前本站,你们在页面上看到的浏览量和数据库中的浏览量并不是一致的。有兴趣的可以点击我的网站玩玩~~
兄弟姐妹们啊,个人网站只是个小项目,纯属为了学习而用的,文章可以看看,但是,就不要抓取了吧。。。。一个小时抓取6万次宝宝心脏真的受不了,虽然服务器一切都还稳定==
个人网站:http://www.wenzhihuai.com
个人网站源码,希望能给个star:https://github.com/Zephery/newblog
参考:
1.《深入理解mybatis原理》 MyBatis的一级缓存实现详解
2.《深入理解mybatis原理》 MyBatis的二级缓存的设计原理
3.聊聊Mybatis缓存机制
4.Spring思维导图
5.SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
6.《深入分布式缓存:从原理到实践》