Help
RSS
API
Feed
Maltego
Contact
Domain > 979137.com
×
Welcome!
Right click nodes and scroll the mouse to navigate the graph.
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2016-09-29
220.181.136.41
(
ClassC
)
2016-09-30
220.181.136.27
(
ClassC
)
2016-10-04
220.181.136.55
(
ClassC
)
2016-10-22
220.181.136.30
(
ClassC
)
2024-07-13
101.32.186.164
(
ClassC
)
Port 80
HTTP/1.1 301 Moved PermanentlyServer: nginxDate: Sat, 13 Jul 2024 08:04:46 GMTContent-Type: text/htmlContent-Length: 162Connection: keep-aliveLocation: https://979137.com/Strict-Transport-Security: max-age31536000 html>head>title>301 Moved Permanently/title>/head>body>center>h1>301 Moved Permanently/h1>/center>hr>center>nginx/center>/body>/html>
Port 443
HTTP/1.1 200 OKServer: nginxDate: Sat, 13 Jul 2024 08:04:47 GMTContent-Type: text/html; charsetUTF-8Transfer-Encoding: chunkedConnection: keep-aliveVary: Accept-EncodingLink: https://979137.com/wp-json/>; relhttps://api.w.org/Strict-Transport-Security: max-age31536000 !DOCTYPE html>html classmodern pc dirltr langzh-CN>head>meta charsetUTF-8>!--if IE>meta http-equivX-UA-Compatible contentIEedge>!endif-->meta nameviewport contentwidthdevice-width,initial-scale1.0>link relpingback hrefhttps://979137.com/xmlrpc.php> !-- All in One SEO 4.5.6 - aioseo.com --> title>首页 | 云厉的博客/title> meta namedescription content一个简单、纯粹的技术博客!专注于云计算、Linux、Python、Go、PHP、Web应用等技术架构 /> meta namerobots contentmax-image-preview:large /> meta namekeywords content云计算,云平台,php,php框架,php教程,thinkphp,codeigniter,python,golang,linux,vim,apache,nginx,mysql,memcache,redis,mongodb,web开发,web应用,html,css,js,javascript,jquery,discuz,api store,技术博客,开发者,大数据,terraform,provider,golang,腾讯云,基础架构,资源编排,apache,nginx,按天分日志,日志,定时清日志 /> link relcanonical hrefhttps://979137.com /> link relnext hrefhttps://979137.com/page/2 /> meta namegenerator contentAll in One SEO (AIOSEO) 4.5.6 /> script typeapplication/ld+json classaioseo-schema> {@context:https:\/\/schema.org,@graph:{@type:BreadcrumbList,@id:https:\/\/979137.com#breadcrumblist,itemListElement:{@type:ListItem,@id:https:\/\/979137.com\/#listItem,position:1,name:\u4e3b\u9801}},{@type:CollectionPage,@id:https:\/\/979137.com#collectionpage,url:https:\/\/979137.com,name:\u9996\u9875 | \u4e91\u5389\u7684\u535a\u5ba2,description:\u4e00\u4e2a\u7b80\u5355\u3001\u7eaf\u7cb9\u7684\u6280\u672f\u535a\u5ba2\uff01\u4e13\u6ce8\u4e8e\u4e91\u8ba1\u7b97\u3001Linux\u3001Python\u3001Go\u3001PHP\u3001Web\u5e94\u7528\u7b49\u6280\u672f\u67b6\u6784,inLanguage:zh-CN,isPartOf:{@id:https:\/\/979137.com\/#website},breadcrumb:{@id:https:\/\/979137.com#breadcrumblist},about:{@id:https:\/\/979137.com\/#organization}},{@type:Organization,@id:https:\/\/979137.com\/#organization,name:\u4e91\u5389\u7684\u535a\u5ba2,url:https:\/\/979137.com\/},{@type:WebSite,@id:https:\/\/979137.com\/#website,url:https:\/\/979137.com\/,name:\u4e91\u5389\u7684\u535a\u5ba2,description:\u4e00\u4e2a\u7b80\u5355\u3001\u7eaf\u7cb9\u7684\u6280\u672f\u535a\u5ba2\uff01\u4e13\u6ce8\u4e8e\u4e91\u8ba1\u7b97\u3001Linux\u3001Python\u3001Go\u3001PHP\u3001Web\u5e94\u7528\u7b49\u6280\u672f\u67b6\u6784,inLanguage:zh-CN,publisher:{@id:https:\/\/979137.com\/#organization},potentialAction:{@type:SearchAction,target:{@type:EntryPoint,urlTemplate:https:\/\/979137.com\/?s{search_term_string}},query-input:required namesearch_term_string}}} /script> !-- All in One SEO -->link reldns-prefetch href//s.w.org />link relalternate typeapplication/rss+xml title云厉的博客 » Feed hrefhttps://979137.com/feed />link relalternate typeapplication/rss+xml title云厉的博客 » 评论Feed hrefhttps://979137.com/comments/feed />script typetext/javascript>window._wpemojiSettings {baseUrl:https:\/\/s.w.org\/images\/core\/emoji\/14.0.0\/72x72\/,ext:.png,svgUrl:https:\/\/s.w.org\/images\/core\/emoji\/14.0.0\/svg\/,svgExt:.svg,source:{concatemoji:https:\/\/979137.com\/wp-includes\/js\/wp-emoji-release.min.js?ver6.0.5}};/*! This file is auto-generated */!function(e,a,t){var n,r,o,ia.createElement(canvas),pi.getContext&&i.getContext(2d);function s(e,t){var aString.fromCharCode,e(p.clearRect(0,0,i.width,i.height),p.fillText(a.apply(this,e),0,0),i.toDataURL());return p.clearRect(0,0,i.width,i.height),p.fillText(a.apply(this,t),0,0),ei.toDataURL()}function c(e){var ta.createElement(script);t.srce,t.defert.typetext/javascript,a.getElementsByTagName(head)0.appendChild(t)}for(oArray(flag,emoji),t.supports{everything:!0,everythingExceptFlag:!0},r0;ro.length;r++)t.supportsorfunction(e){if(!p||!p.fillText)return!1;switch(p.textBaselinetop,p.font600 32px Arial,e){caseflag:return s(127987,65039,8205,9895,65039,127987,65039,8203,9895,65039)?!1:!s(55356,56826,55356,56819,55356,56826,8203,55356,56819)&&!s(55356,57332,56128,56423,56128,56418,56128,56421,56128,56430,56128,56423,56128,56447,55356,57332,8203,56128,56423,8203,56128,56418,8203,56128,56421,8203,56128,56430,8203,56128,56423,8203,56128,56447);caseemoji:return!s(129777,127995,8205,129778,127999,129777,127995,8203,129778,127999)}return!1}(or),t.supports.everythingt.supports.everything&&t.supportsor,flag!or&&(t.supports.everythingExceptFlagt.supports.everythingExceptFlag&&t.supportsor);t.supports.everythingExceptFlagt.supports.everythingExceptFlag&&!t.supports.flag,t.DOMReady!1,t.readyCallbackfunction(){t.DOMReady!0},t.supports.everything||(nfunction(){t.readyCallback()},a.addEventListener?(a.addEventListener(DOMContentLoaded,n,!1),e.addEventListener(load,n,!1)):(e.attachEvent(onload,n),a.attachEvent(onreadystatechange,function(){completea.readyState&&t.readyCallback()})),(et.source||{}).concatemoji?c(e.concatemoji):e.wpemoji&&e.twemoji&&(c(e.twemoji),c(e.wpemoji)))}(window,document,window._wpemojiSettings);/script>style typetext/css>img.wp-smiley,img.emoji { display: inline !important; border: none !important; box-shadow: none !important; height: 1em !important; width: 1em !important; margin: 0 0.07em !important; vertical-align: -0.1em !important; background: none !important; padding: 0 !important;}/style> link relstylesheet idwpa-css-css hrefhttps://979137.com/wp-content/plugins/wp-attachments/styles/0/wpa.css?ver6.0.5 typetext/css mediaall />link relstylesheet idstyle-css hrefhttps://979137.com/wp-content/themes/cubic/style.css?ver3.1.4 typetext/css mediaall />link relstylesheet idwp-block-library-css hrefhttps://979137.com/wp-includes/css/dist/block-library/style.min.css?ver6.0.5 typetext/css mediaall />style idglobal-styles-inline-css typetext/css>body{--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--duotone--dark-grayscale: url(#wp-duotone-dark-grayscale);--wp--preset--duotone--grayscale: url(#wp-duotone-grayscale);--wp--preset--duotone--purple-yellow: url(#wp-duotone-purple-yellow);--wp--preset--duotone--blue-red: url(#wp-duotone-blue-red);--wp--preset--duotone--midnight: url(#wp-duotone-midnight);--wp--preset--duotone--magenta-yellow: url(#wp-duotone-magenta-yellow);--wp--preset--duotone--purple-green: url(#wp-duotone-purple-green);--wp--preset--duotone--blue-orange: url(#wp-duotone-blue-orange);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;}/style>style idmd-style-inline-css typetext/css> /style>script typetext/javascript srchttps://979137.com/wp-includes/js/jquery/jquery.min.js?ver3.6.0 idjquery-core-js>/script>script typetext/javascript srchttps://979137.com/wp-includes/js/jquery/jquery-migrate.min.js?ver3.3.2 idjquery-migrate-js>/script>link relhttps://api.w.org/ hrefhttps://979137.com/wp-json/ />script srchttps://979137.com/wp-content/themes/cubic/js/jquery.easing.1.4.js?ver3.1.4>/script>script srchttps://979137.com/wp-content/themes/cubic/js/jscript.js?ver3.1.4>/script>link relstylesheet hrefhttps://979137.com/wp-content/themes/cubic/color/blue.css?ver3.1.4>link relstylesheet mediascreen and (max-width:770px) hrefhttps://979137.com/wp-content/themes/cubic/responsive.css?ver3.1.4>link href//fonts.googleapis.com/css?familyPoiret+One relstylesheet typetext/css>link href//fonts.googleapis.com/css?familyOswald:300 relstylesheet typetext/css>style>#post_list .title { font-size:21px; }#post_list .post_content { font-size:14px; }#logo { font-family:Poiret One,Arial,sans-serif; }#related_post .headline, .side_headline, #comment_header .headline, .footer_headline { font-size:18px; text-transform:uppercase; font-family:Oswald,Arial,sans-serif; }.pc #logo_text { position:absolute; top:76px; left:0px; }.pc #logo_text #logo { float:left; }.pc #logo_text #desc { margin-top:17px; float:left; }.pc #logo { font-size:42px; }.mobile #logo_text #logo, .mobile #logo_image a:before { font-size:24px; }.post_content a { color:#333333; }#logo_text #logo a:hover, a:hover, .post_meta li a:hover, .side_widget a:hover, #footer a:hover, .iw_social_link li a:hover:after, #bread_crumb .home a:hover i:before, #comment_user_login span a:hover:after, .page_navi p.back a:hover, .page_navi p.back a:hover:after, #bread_crumb ol li a:hover { color:#009DC4; }.pc #search_area_top .search_button input:hover, #comment_header #comment_tab li.active a, #guest_info input:focus, #comment_textarea textarea:focus, .ml_user_profile_widget li.mail_button a:hover, #profile_page_top li.mail_button a:hover { border-color:#009DC4; }#submit_comment_wrapper #submit_comment:hover, .comment_meta a:hover, #cancel_comment_reply a:hover, #respond.comment-respond #submit:hover, #comment_pager a.page-numbers:hover, .commentlist .child_menu_button:hover, .commentlist .child_menu_button.active, .comment_edit a:hover, .comment_reply a:hover, .post_content .more-link:hover, #next_prev_post a:hover, .post_pagination a:hover, #return_top:hover, #search_area_top .search_button input:hover, .mobile #footer_menu li a:hover, #profile_page_top li.mail_button a:hover, .widget_search #search-btn input:hover, .widget_search #searchsubmit:hover, .menu_button:hover, .iw_search_area .search_button input:hover, #wp-calendar td a:hover, #wp-calendar #prev a:hover, #wp-calendar #next a:hover, .ml_user_profile_widget li.mail_button a:hover { background-color:#009DC4; }.widget_post_list .image:after, #related_post .image:after { background:rgba(0,157,196,0); }.widget_post_list .image:hover:after, #related_post .image:hover:after { background:rgba(0,157,196,0.8); }.commentlist .child_menu_button.active:after { border-color:#009DC4 transparent transparent transparent; }/style>/head>body classhome blog fixed_header has_bread> div idcontainer> header idheader_top classclearfix> !-- logo --> !-- header menu --> nav idheader_menu classclearfix> ul idmenu-%e9%a1%b6%e9%83%a8%e8%8f%9c%e5%8d%95 classmenu>li idmenu-item-252 classmenu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-252>a href/ aria-currentpage>首页/a>/li>li idmenu-item-261 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-261>a hrefhttps://979137.com/archives/category/cloud>云计算/a>/li>li idmenu-item-253 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-253>a hrefhttps://979137.com/archives/category/system>UNIX/Linux/a>/li>li idmenu-item-257 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-257>a hrefhttps://979137.com/archives/category/python>Python/a>/li>li idmenu-item-256 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-256>a hrefhttps://979137.com/archives/category/php>PHP/a>/li>li idmenu-item-631 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-631>a hrefhttps://979137.com/archives/category/golang>Golang/a>/li>li idmenu-item-254 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-254>a hrefhttps://979137.com/archives/category/server>Apache/Nginx/a>/li>li idmenu-item-255 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-255>a hrefhttps://979137.com/archives/category/db>数据库/a>/li>li idmenu-item-258 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-258>a hrefhttps://979137.com/archives/category/web>Web开发/a>/li>li idmenu-item-554 classmenu-item menu-item-type-taxonomy menu-item-object-category menu-item-554>a hrefhttps://979137.com/archives/category/essay>随笔/a>/li>/ul> /nav> !-- search and social link --> div idsearch_link_area> ul classclearfix> li classrss_button>a target_blank hrefhttps://979137.com/feed>span>rss/span>/a>/li> /ul> !-- search area --> div idsearch_area_top classclearfix> div classsearch_form> form methodget actionhttps://979137.com/> div classsearch_input>input typetext value names placeholderSEARCH />/div> div classsearch_button>input typesubmit valueSearch />/div> /form> /div> /div>!-- END #search_area_top --> /div>!-- END #search_link_area --> /header> div idheader> div idheader_inner classclearfix> !-- logo --> div idlogo_text> h1 idlogo>a hrefhttps://979137.com/>云厉的博客/a>/h1> h2 iddesc>一个简单、纯粹的技术博客!专注于云计算、Linux、Python、Go、PHP、Web应用等技术架构/h2>/div> /div> /div> !-- global menu --> !-- bread crumb --> !-- banner --> !-- banner --> div idcontents classclearfix> div idmain_contents classclearfix>div idmain_col> section idpost_list classclearfix> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/1135.html titleZooKeeper是如何实现数据一致性的?>ZooKeeper是如何实现数据一致性的?/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2019-02-26T14:45:16+08:00>2019.02.26/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/cloud relcategory tag>云计算/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/observer reltag>observer/a>a hrefhttps://979137.com/archives/tag/zab reltag>ZAB/a>a hrefhttps://979137.com/archives/tag/zookeeper reltag>ZooKeeper/a>/li> /ul> /div> div classpost_content clearfix> p>众所周知,ZooKeeper 是一个开源的分布式协调服务,很多分布式的应用都是基于 ZooKeeper 来实现分布式锁、服务管理、通知订阅等功能。br />那么 ZooKeeper 自身是如何在分布式环境下实现数据的一致性的呢?/p>h3>结构/h3>p>既然 ZooKeeper 是在分布式环境下提供服务的,那么它必须要解决的问题就是单点问题,因此 ZooKeeper 是一个主备的结构。ZooKeeper 存在 leader、follower、observer三种角色,这三种角色在实际服务集群中都是服务节点。/p>ul>li>leader:处理所有请求,为客户的提供读和写服务/li>li>follower:只提供读服务,有机会通过选举成为leader/li>li>observer:只提供读服务/li>/ul>p>由以上三种角色的介绍可知,ZooKeeper 中所有请求都是交给 leader 处理的,因此,如果leader挂了,ZooKeeper 就无法再提供服务,这就是单点问题。所幸在leader 挂了之后,followe r能够通过选举成为新的 leader。br />那么问题来了,follower 是如何被选举成为新的 leader 的?新的 leader 又是如何保证数据的一致性的?这些问题的答案都在于 ZooKeeper 所用的分布式一致性协议ZAB。/p>h3>ZAB协议/h3>p>ZAB协议是为 ZooKeeper 专门设计的一种支持奔溃恢复的原子广播协议。虽然它不像 Paxos 算法那样通用通用,但是它却比 Paxos 算法易于理解。在我看来ZAB协议主要的作用在于三个方面:/p>ol>li>选举出 leader/li>li>同步节点之间的状态达到数据一致/li>li>数据的广播/li>/ol>p>在了解ZAB协议具体过程之前不要先了解几个概念。/p>h5>事务/h5>p>ZooKeeper 作为一个分布式协调服务,需要 leader 节点去接受外部请求,转化为内部操作(比如创建,修改,删除节点),需要原子性执行的几个操作就组成了事务,这里用 T 代表。ZooKeeper 要求有序的处理事务,因此给每个事务一个编号,每进来一个事务编号加1,假设 leader 节点中进来 n 个事务,可以表示为 T1,T2,T3…Tn。为了防止单点问题导致事务数据丢失,leader 节点会把事务数据同步到 follower 节点中。/p>h5>事务队列/h5>p>leader 和 follower 都会保存一个事务队列,用 L 表示,LT1,T2,T3…Tn,leader 的事务队列是接受请求保存下来的,follower 的事务队列是 leader 同步过来的,因此leader 的事务队列是最新,最全的。/p>h5>任期/h5>p>在 ZooKeeper 的工作过程中,leader 节点奔溃,重新选举出新的 leader 是很正常的事情,所以 ZooKeeper 的运行历史中会产生多个 leader,就好比一个国家的历史中会相继出现多为领导人。为了区分各个 leader,ZAB协议用一个整数来表示任期,我们假设用E表示任务。ZooKeeper 刚运行时选举出的第一个 leader 的任期为E1;第一个 leader 奔溃后,下面选举出来的 leader,任期会加1,E2;一次类推。加入任期概念的事务应该表示为 T(E,n)/p>h3>协议过程/h3>p>/p>h5>选举leader/h5>ol>li>每个 follower 广播自己事务队列中最大事务编号 maxId/li>li>获取集群中其他 follower 发出来的 maxId,选取出最大的 maxId 所属的 follower,投票给该 follower,选它为 leader/li>li>统计所有投票,获取投票数超过一半的 follower 被推选为 leader/li>/ol>h5>同步数据/h5>ol>li>各个 follower 向 leader 发送自己保存的任期E/li>li>leader,比较所有的任期,选取最大的E,加1后作为当前的任期EE+1/li>li>将任务E广播给所有follower/li>li>follower 将任期改为 leader 发过来的值,并且返回给 leader 事务队列 L/li>li>leader 从队列集合中选取任期最大的队列,如果有多个队列任期都是最大,则选取事务编号 n 最大的队列 Lmax。将 Lmax 置为 leader 队列,并且广播给各个 follower。/li>li>follower 接收队列替换自己的事务队列,并且执行提交队列中的事务。/li>/ol>p>至此各个节点的数据达成一致,ZooKeeper 恢复正常服务。/p>h5>广播/h5>ol>li>leader 节点接收到请求,将事务加入事务队列,并且将事务广播给各个 follower。/li>li>follower 接收事务并加入都事务队列,然后给 leader 发送准备提交请求。/li>li>leader 接收到半数以上的准备提交请求后,提交事务同时向 follower 发送提交事务请求/li>li>follower 提交事务。/li>/ol> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/1084.html titlePython实战之520特别版:用微信每天和她说晚安>Python实战之520特别版:用微信每天和她说晚安/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2018-05-20T00:23:31+08:00>2018.05.20/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/python relcategory tag>Python/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/python reltag>Python/a>a hrefhttps://979137.com/archives/tag/crawler reltag>爬虫/a>/li> /ul> /div> div classpost_content clearfix> p>今天是传说中的font color#0082ef>520/font>,不知你是否已经准备好要表白的话语。为了助你撩妹成功,云厉今天也学着某些人土土的教大家用Python每天给妹纸说晚安。br />没错,每天!br />用对了Python,每天都过font color#0082ef>520/font>!/p>p>这次真的是font color#0082ef>面向对象编程/font>!/p>p>大致思路是这样:/p>ol>li>调接口获取每日心灵鸡汤或撩妹话术/li>li>基于wxpy模块,授权微信登录,搜索聊天对象,发送XX消息/li>li>Timer每日发送/li>li>后台挂起执行脚本/li>/ol>p>安装font color#0082ef>wxpy/font>和font color#0082ef>requests/font>,/p>pre>code classlanguage-shell>pip3 install wxpypip3 install requests/code>/pre>p>其他如果没安装,也一并安装/p>p>下面发送信息脚本font color#0082ef>send_wx.py/font>,不到40行的代码,注释不多,相信你懂!/p>pre>code classlanguage-python># -*- coding: utf-8 -*-from threading import Timerfrom wxpy import *import requestsbot Bot(console_qr2, cache_path'botoo.pkl') # 她的昵称和你的昵称。是微信昵称,不是备注哦ta_name '若';my_name '云厉';# 从金山词霸获取每日鸡汤,英文和翻译,如果你有更好的鸡汤数据源,也可以更换def get_message(): url "http://open.iciba.com/dsapi/" r requests.get(url) contents r.json()'content' translation r.json()'translation' return contents, translation# 给TA发送晚安信息def send_message(): try: message get_message() # 搜索聊天对象,并ensure_one保证她的昵称在你的微信好友列表里只有一个 my_love ensure_one(bot.search(ta_name, sexFEMALE)) # 发送鸡汤 my_love.send(message0) my_love.send(message15:) my_love.send(u'亲爱的,晚安!爱你~') # 每86400秒(1天),发送1次 t Timer(86400, send_message) t.start() except: my_self ensure_one(bot.search(my_name, sexMALE)) my_self.send(u'今天撩妹消息发送失败了')if __name__ '__main__': send_message()/code>/pre>pre>code classlanguage-shell># 丢后台跑起来!nohup python3 send_wx.py > send_wx.log 2>&1 &/code>/pre>p>首次运行,脚本会生成二维码,需要授权登录。这也是为什么在脚本内用Timer定时发送而不用Linux的cron,就是为了不要每次都授权登录,太麻烦!br />img srchttps://979137.com/wp-content/uploads/2018/05/115a2878-a3e1-45ca-9970-02e448fd1b9d.png alt />br />img srchttps://979137.com/wp-content/uploads/2018/05/WechatIMG850.jpeg alt />/p>p>测试一把,效果如下:br />img srchttps://979137.com/wp-content/uploads/2018/05/WechatIMG856.jpeg alt />/p>p>font color#0082ef>wxpy/font>还能干啥?/p>ul>li>控制路由器、智能家居等具有开放接口的玩意儿/li>li>运行脚本时自动把日志发送到你的微信/li>li>加群主为好友,自动拉进群中/li>li>跨号或跨群转发消息/li>li>自动陪人聊天/li>li>逗人玩/li>li>.../li>/ul>p>总而言之,可用来实现各种微信个人号的自动化操作/p>p>如果你有兴趣,欢迎关注微信公众号:font color#0082ef>程序员到架构师/font>,不定期更新各种各样的font color#0082ef>Python/font>技术/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/1066.html titlePython中被忽略的else>Python中被忽略的else/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2018-05-06T20:25:38+08:00>2018.05.06/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/wumingxiaoyao title文章作者 无名小妖 relauthor>无名小妖/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/python relcategory tag>Python/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/python reltag>Python/a>/li> /ul> /div> div classpost_content clearfix> p>else, 我们再熟悉不过了。对于一个Python程序员来说,else往往都是配合 if 来使用的,像这样:/p>pre>code classlanguage-python>a '12'if a '123': print(a)else: print('出错了!')/code>/pre>p>/p>p>但是,Python中的else并不只能用在if之后,so,这次我们讨论一下Python流程控制中的else。/p>p>else子句不仅能在 if 语句中使用,还能在 for、while 和 try 语句中使用,这个语言特性不是什么秘密,但却没有得到重视。我们看一个例子:/p>pre>code classlanguage-python>my_list 'apple', 'pear', 'orange', 'banana'for item in my_list: if item 'banana': print('Founded!') breakelse: raise ValueError('No banana flavor found!')/code>/pre>p>/p>p>本例当中,循环最后找到了banana‘,输出Founded!,并且跳出循环,所以else字句并没有被执行。但如果,将代码修改一下,去掉列表中的banana:/p>pre>code classlanguage-python>my_list 'apple', 'pear', 'orange'for item in my_list: if item 'banana': print('Founded!') breakelse: raise ValueError('No banana flavor found!')/code>/pre>p>/p>p>运行代码就会直接抛出错误!如果不使用else字句来完成上述功能,可能我们就需要设置控制标志了,像这样:/p>pre>code classlanguage-python>my_list 'apple', 'pear', 'orange'flag True for item in my_list: if item 'banana': print('Founded!') flag False breakif flag: raise ValueError('No banana flavor found!')/code>/pre>p>/p>p>很明显,这里使用了额外的变量 flag 和 if 语句。/p>p>while 和 for 相类似,简单举个例子:/p>pre>code classlanguage-python>a 'apple'while a 'banana': passelse: raise ValueError('No banana flavor found!')/code>/pre>p>/p>p>下面看一下try:/p>pre>code classlanguage-python>try: dangerous_call()except OSError: log('OSError...')else: after_call()/code>/pre>p>/p>p>很明确,try 块防守的是 dangerouscall() 可能出现的错误,而且很明显,只有 try 块不抛出异常,才会执行 aftercall()。/p>p>现在,总结一下 else 子句的行为如下:/p>p>for: 仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。br />while: 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被break 语句中止)才运行 else 块。br />try: 仅当 try 块中没有异常抛出时才运行 else 块。/p>p>即,如果异常或者 return、break 或 continue 语句导致控制权跳到了复合语句的主块之外,那么else 子句也会被跳过。/p>p>for/else、while/else 和 try/else 的语义关系紧密,不过与if/else 差别很大。主要是else 这个单词的意思阻碍了我们对这些特性的理解。 按正常的理解应该是“要么运行这个循环,要么做那件事”。可是,在循环中,else 的语义恰好相反:“运行这个循环,然后做那件事。”不过,相信多使用几次,你会熟悉的。/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/1039.html title准确判断文件类型方法之二进制文件头>准确判断文件类型方法之二进制文件头/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2018-04-29T23:39:10+08:00>2018.04.29/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/php relcategory tag>PHP/a> a hrefhttps://979137.com/archives/category/python relcategory tag>Python/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/php reltag>PHP/a>a hrefhttps://979137.com/archives/tag/python reltag>Python/a>/li> /ul> /div> div classpost_content clearfix> p>判断文件类型在开发中非常常见的需求,很多开发者的做法是简单判断文件的扩展名,很遗憾,这种方法非常不靠谱,因为文件扩展名可以伪造,并且在Linux系统中,没有扩展名的概念!/p>p>这里推荐一种准确判断文件类型的方法:font color#0082ef>通过二进制文件头判断文件类型/font>br />代码非常简单,只需从文件流中读取前两个字节,再解包,即可判断出文件类型,并且大部分语言都支持,Python和PHP版示例参考:/p>h4>Python版/h4>pre classprism-highlight line-numbers>code classlanguage-python># -*- coding: utf-8 -*-import struct file_type_map { 7790 : exe, 7784 : midi, 8075 : zip, 8297 : rar, 7173 : gif, 6677 : bmp, 13780 : png, 255216 : jpg,}def get_file_type(filename): fp open(filename, rb) str_info struct.unpack_from(BB, fp.read(2)) str_code %d%d % (str_info0, str_info1) fp.close() return unknown if str_code not in file_type_map else file_type_mapstr_codeif __name__ __main__: file_type get_file_type(./test.png) print(file_type)/code>/pre>p>/p>h4>PHP版/h4>p>PHP有一个内置的专门用于判断图像类型的方法,code>exif_imagetype/code>,默认未开启,需要编译时候指定 code>--enable-exif/code> 开启,并且只支持图像类型文件,所以不推荐使用。PHP同样可以通过二进制文件头判断文件类型/p>pre classprism-highlight line-numbers>code classlanguage-php><?php$file_type_map array( 7790 > exe, 7784 > midi, 8075 > zip, 8297 > rar, 7173 > gif, 6677 > bmp, 13780 > png, 255216 > jpg,);function get_file_type($file) { $filepath realpath($file); if (!($fp @fopen($filepath, rb))) return false; $bin fread($fp, 2); fclose($fp); $str_info @unpack(C2chars, $bin); $str_code intval($str_infochars1.$str_infochars2); global $file_type_map; return $file_type_map$str_code ?: unknown;}$file_type get_file_type(./test.png);print($file_type);/code>/pre> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/870.html title巧用Terraform完成腾讯云上自动运维>巧用Terraform完成腾讯云上自动运维/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2018-04-22T16:56:44+08:00>2018.04.22/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/golang relcategory tag>Golang/a> a hrefhttps://979137.com/archives/category/cloud relcategory tag>云计算/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/terraform reltag>Terraform/a>a hrefhttps://979137.com/archives/tag/infrastructure reltag>基础架构/a>a hrefhttps://979137.com/archives/tag/tencent_cloud reltag>腾讯云/a>a hrefhttps://979137.com/archives/tag/op reltag>运维/a>/li> /ul> /div> div classpost_content clearfix> p>Terraform是一个IT基础架构自动化编排工具,主张font color#0082ef>基础架构即代码/font>,你可以用代码集中管理你的云资源和基础架构。本文就腾讯云为例,讲述如何用Terraform完成云上自动化运维/p>h4>1. 什么是资源/h4>p>基础设施和服务统称为资源,如私有网络、子网、物理机、虚拟机、镜像、专线、NAT网关等等都可以称之为资源,也是开发和运维人员经常要打交道要维护的东西。br />Terraform把资源大致分为两种:/p>h6>1.1 resource/h6>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource 资源类名 映射到本地的唯一资源名 { 参数 值 ...}/code>/pre>p>这类资源一般是抽象的真正的云服务资源,支持增删改,如私有网络、NAT网关、虚拟机实例/p>h6>1.2 data source/h6>pre classline-numbers prism-highlight data-start1>code classlanguage-json>data 资源类名 映射到本地的唯一资源名 { 参数 值 ...}/code>/pre>p>这类资源一般是固定的一些可读资源,如可用区列表、镜像列表。大部分情况下,resource资源也会封装一个data source方法,用于资源查询/p>h4>2. 准备工作/h4>h6>2.1 安装Go和Terraform/h6>ul>li>Go 1.9 (to build the provider plugin)/li>li>a target_blank hrefhttps://www.terraform.io/downloads.html>Terraform/a> 0.11.x/li>/ul>h6>2.2 下载插件/h6>p>下载腾讯云Terraform插件a target_blank hrefhttp://979137.com/urltransfer/fd2a2ed9d3848e79a833be211c15f992>terraform-provider-tencentcloud/a>,解压到指定目录,给二进制文件设置code>terraform-provider-tencentcloud/code>可执行权限br />(腾讯云正式入驻Terraform官方Providers后,不再需要手工下载插件,Terraform会自动识别资源商插件)/p>h6>2.3 公共配置/h6>p>Terraform实际是对上游API的抽象,你用Terraform的所有操作,最终都是通过API反应到服务商,因此我们需要配置几个公共配置,这里包括用户信息和地域信息,这些公共配置存储在环境变量供code>terraform-provider-tencentcloud/code>读取/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>export TENCENTCLOUD_SECRET_IDyour-awesome-secret-idexport TENCENTCLOUD_SECRET_KEYyour-awesome-secret-keyexport TENCENTCLOUD_REGIONap-guangzhou/code>/pre>p>获取云API秘钥:a target_blank hrefhttps://console.cloud.tencent.com/cam/capi>https://console.cloud.tencent.com/cam/capi/a>/p>h6>2.4 了解几个常用命令/h6>p>Terraform有些命令,是我们常用的,也是后面我们实例会用到的/p>pre classline-numbers prism-highlight data-start1>code classlanguage-null>terraform init # 初始化工作目录,也是我们第一个要执行的命令terraform plan # 生成计划terraform appy # 提交请求terraform state # 查看资源状态terraform graph # 生成执行计划图/code>/pre>p>/p>p>接下来,我们就用code>Terraform/code>构建一个常见的网络项目实例/p>h4>3. 查询资源/h4>p>我们把资源依赖的上游资源,先查询出来,便宜后面引用。/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json># 查询可用区信息data tencentcloud_availability_zones favorate_zones {}# 查询镜像data tencentcloud_image my_favorate_image { filter { name image-type values PUBLIC_IMAGE }}/code>/pre>p>后面我们创建的子网和虚拟机,需要用到可用区和镜像,所以这里先用data查询/p>h4>4. 创建资源/h4>p>4.1 创建一个私有网络,并命名为main,后面创建的其他资源,都会引用这个资源/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_vpc main { name 979137_test_vpc cidr_block 10.6.0.0/16}/code>/pre>p>解读:创建一个名为code>YunLi_test/code>,CIDR为code>10.6.0.0/16/code>的私有网络,在本地命名为code>main/code>,这里会返回包括私有网络ID在内所有VPC属性/p>p>4.2 在私有网络code>main/code>上再创建一个子网,因为虚拟机是挂在子网下的/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_subnet main_subnet { vpc_id ${tencentcloud_vpc.main.id} name 979137_test_subnet cidr_block 10.6.7.0/24 availability_zone ${data.tencentcloud_availability_zones.favorate_zones.zones.0.name}}/code>/pre>p>解读:code>vpc_id/code>引用了5.1我们创建的code>VPC ID/code>,CIDR在私有网络范围内,可用区我们用前面查询到的,因为是示例,所以这里随机用查询到的第一个镜像/p>p>4.3 创建两个弹性IP,关联至NAT网关/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_eip eip_dev_dnat { name 979137_test_eip}resource tencentcloud_eip eip_test_dnat { name 979137_test_eip}/code>/pre>p>解读:所有依赖关系,被依赖的资源都需要先创建/p>p>4.4 创建NAT网关,用于给CVM提供外网能力/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_nat_gateway my_nat { vpc_id ${tencentcloud_vpc.main.id} name 979137_test_nat max_concurrent 3000000 bandwidth 500 assigned_eip_set ${tencentcloud_eip.eip_dev_dnat.public_ip}, ${tencentcloud_eip.eip_test_dnat.public_ip}, }/code>/pre>p>解读:引用了code>VPC ID/code>;指定了最大并发连接数和带宽上限;关联了两个弹性IP,两个弹性IP引用的是前面创建好的弹性IP资源/p>p>4.5 创建一个安全组并配置安全组规则/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_security_group my_sg { name 979137_test_sg description 979137_test_sg}# 放通80,443端口resource tencentcloud_security_group_rule web { security_group_id ${tencentcloud_security_group.my_sg.id} type ingress cidr_ip 0.0.0.0/0 ip_protocol tcp port_range 80,443 policy accept}# 放通常用web端口resource tencentcloud_security_group_rule sg_web { security_group_id ${tencentcloud_security_group.my_sg.id} type ingress cidr_ip 0.0.0.0/0 ip_protocol tcp port_range 80,443,8080 policy accept}# 放通内网ssh登录resource tencentcloud_security_group_rule sg_ssh { security_group_id ${tencentcloud_security_group.my_sg.id} type ingress cidr_ip 10.65.0.0/16 ip_protocol tcp port_range 22 policy accept}# 拒绝所有访问resource tencentcloud_security_group_rule sg_drop { security_group_id ${tencentcloud_security_group.my_sg.id} type ingress cidr_ip 0.0.0.0/0 ip_protocol tcp port_range ALL policy drop}/code>/pre>p>解读:安全策略是云上安全必不可少的一环,这里除了允许内网ssh登录和开放web端口,其他全部拒绝/p>p>4.6 创建虚拟机/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_instance foo { availability_zone ${data.tencentcloud_availability_zones.favorate_zones.zones.0.name} image_id ${data.tencentcloud_image.my_favorate_image.image_id} vpc_id ${tencentcloud_vpc.main.id} subnet_id ${tencentcloud_subnet.main_subnet.id} security_groups ${tencentcloud_security_group.my_sg.id}, }/code>/pre>p>解读:可用区我们和子网用了同一个(而且必须是同一个),因为是示例我们使用了data查询到的第一个镜像,把虚拟机放在了指定的VPC子网内,关联了一个安全组/p>p>4.7 增加NAT网关端口转发规则/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>resource tencentcloud_dnat dev_dnat { vpc_id ${tencentcloud_nat_gateway.my_nat.vpc_id} nat_id ${tencentcloud_nat_gateway.my_nat.id} protocol tcp elastic_ip ${tencentcloud_eip.eip_dev_dnat.public_ip} elastic_port 80 private_ip ${tencentcloud_instance.foo.private_ip} private_port 9001}resource tencentcloud_dnat test_dnat { vpc_id ${tencentcloud_nat_gateway.my_nat.vpc_id} nat_id ${tencentcloud_nat_gateway.my_nat.id} protocol udp elastic_ip ${tencentcloud_eip.eip_test_dnat.public_ip} elastic_port 8080 private_ip ${tencentcloud_instance.foo.private_ip} private_port 9002}/code>/pre>p>解读:这里引用了关系较多,端口转发的本质是将内网虚拟机IP/端口映射到外网弹性IP的端口。/p>p>全部配置写完后,执行code>plan/code>:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>An execution plan has been generated and is shown below.Resource actions are indicated with the following symbols: + createTerraform will perform the following actions: + tencentcloud_dnat.dev_dnat id: <computed> elastic_ip: ${tencentcloud_eip.eip_dev_dnat.public_ip} elastic_port: 80 nat_id: ${tencentcloud_nat_gateway.my_nat.id} private_ip: ${tencentcloud_instance.foo.private_ip} private_port: 9001 protocol: tcp vpc_id: ${tencentcloud_nat_gateway.my_nat.vpc_id} + tencentcloud_dnat.test_dnat id: <computed> elastic_ip: ${tencentcloud_eip.eip_test_dnat.public_ip} elastic_port: 8080 nat_id: ${tencentcloud_nat_gateway.my_nat.id} private_ip: ${tencentcloud_instance.foo.private_ip} private_port: 9002 protocol: udp vpc_id: ${tencentcloud_nat_gateway.my_nat.vpc_id} + tencentcloud_eip.eip_dev_dnat id: <computed> name: 979137_test_eip public_ip: <computed> status: <computed> + tencentcloud_eip.eip_test_dnat id: <computed> name: 979137_test_eip public_ip: <computed> status: <computed> + tencentcloud_instance.foo id: <computed> allocate_public_ip: false availability_zone: ap-guangzhou-2 data_disks.#: <computed> image_id: img-b4tgwxvn instance_name: CVM-Instance instance_status: <computed> key_name: <computed> private_ip: <computed> public_ip: <computed> security_groups.#: <computed> subnet_id: ${tencentcloud_subnet.main_subnet.id} system_disk_size: <computed> system_disk_type: <computed> vpc_id: ${tencentcloud_vpc.main.id} + tencentcloud_nat_gateway.my_nat id: <computed> assigned_eip_set.#: <computed> bandwidth: 500 max_concurrent: 3000000 name: 979137_test_nat vpc_id: ${tencentcloud_vpc.main.id} + tencentcloud_security_group.my_sg id: <computed> description: 979137_test_sg name: 979137_test_sg + tencentcloud_security_group_rule.sg_drop id: <computed> cidr_ip: 0.0.0.0/0 ip_protocol: tcp policy: drop security_group_id: ${tencentcloud_security_group.my_sg.id} type: ingress + tencentcloud_security_group_rule.sg_ssh id: <computed> cidr_ip: 10.65.0.0/16 ip_protocol: tcp policy: accept port_range: 22 security_group_id: ${tencentcloud_security_group.my_sg.id} type: ingress + tencentcloud_security_group_rule.sg_web id: <computed> cidr_ip: 0.0.0.0/0 ip_protocol: tcp policy: accept port_range: 80,443,8080 security_group_id: ${tencentcloud_security_group.my_sg.id} type: ingress + tencentcloud_subnet.main_subnet id: <computed> availability_zone: ap-guangzhou-2 cidr_block: 10.6.7.0/24 name: 979137_test_subnet route_table_id: <computed> vpc_id: ${tencentcloud_vpc.main.id} + tencentcloud_vpc.main id: <computed> cidr_block: 10.6.0.0/16 is_default: <computed> is_multicast: <computed> name: 979137_test_vpcPlan: 12 to add, 0 to change, 0 to destroy./code>/pre>p>解读:12个资源将被创建,0个变更,0个销毁/p>p>确认无误,执行code>apply/code>,可以看到执行结果br />Apply complete! Resources: 12 added, 0 changed, 0 destroyed./p>p>我们可以通过code>graph/code>命令结合code>graphviz/code>工具生成资源执行计划图/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>terraform graph | dot -Tsvg > graph.svg/code>/pre>p>img srchttps://979137.com/wp-content/uploads/2018/04/graph.png alt />/p>h4>5. 更新资源/h4>p>更新资源,就是更新前面我们写的配置的参数,这可能就是Terraform魅力之一吧!br />如:我要更新端口转发规则code>test_dnat/code>的协议和外部端口两个参数、安全组code>my_sg/code>的名字和备注信息。修改code>tf/code>文件后执行code>plan/code>,出现了要更新的资源:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>-/+ tencentcloud_dnat.test_dnat (new resource required) id: tcp://vpc-5ooh0ivd:nat-dn2bdr68@139.199.230.14:8080 > <computed> (forces new resource) elastic_ip: 139.199.230.14 > 139.199.230.14 elastic_port: 8080 > 443 (forces new resource) nat_id: nat-dn2bdr68 > nat-dn2bdr68 private_ip: 10.6.7.15 > 10.6.7.15 private_port: 9002 > 9002 protocol: tcp > tcp vpc_id: vpc-5ooh0ivd > vpc-5ooh0ivd ~ tencentcloud_security_group.my_sg description: 979137_test_sg > 979137_dev_sg name: 979137_test_sg > 979137_dev_sgPlan: 1 to add, 1 to change, 1 to destroy./code>/pre>p>和创建一样,执行code>apply/code>,提交修改/p>p>思考:我修改两个资源,为什么变成1个添加,1个修改,1个销毁?br />font color#0082ef>解答:这是Terraform的code>ForceNew/code>机制,端口转发规则的修改等价于删除+创建,我在《a target_blank hrefhttps://979137.com/archives/518.html>font color#0082ef>腾讯云支持Terraform开发实践/font>/a>》文中详细阐述过Terraform工作原理,欢迎阅读/font>/p>h4>6. 删除资源/h4>p>Terraform删除资源,有两种方式/p>h6>6.1 注释要删除的资源/h6>p>Terraform注释不只对参数有效,还对整个资源配置有效,比如我注释一个DNAT资源/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>#resource tencentcloud_dnat dev_dnat {# vpc_id ${tencentcloud_nat_gateway.my_nat.vpc_id}# nat_id ${tencentcloud_nat_gateway.my_nat.id}# protocol tcp# elastic_ip ${tencentcloud_eip.eip_dev_dnat.public_ip}# elastic_port 80# private_ip ${tencentcloud_instance.foo.private_ip}# private_port 9001#}/code>/pre>p>执行code>plan/code>可以看到,/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>Terraform will perform the following actions: - tencentcloud_dnat.dev_dnatPlan: 0 to add, 0 to change, 1 to destroy./code>/pre>p>Terraform认为code>dev_dnat/code>是要删除的资源/p>h6>6.2 terraform destory/h6>pre classline-numbers prism-highlight data-start1>code classlanguage-json>Terraform will perform the following actions: - tencentcloud_dnat.dev_dnat - tencentcloud_dnat.test_dnat - tencentcloud_eip.eip_dev_dnat - tencentcloud_eip.eip_test_dnat - tencentcloud_instance.foo - tencentcloud_nat_gateway.my_nat - tencentcloud_security_group.my_sg - tencentcloud_security_group_rule.sg_drop - tencentcloud_security_group_rule.sg_ssh - tencentcloud_security_group_rule.sg_web - tencentcloud_subnet.main_subnet - tencentcloud_vpc.mainPlan: 0 to add, 0 to change, 12 to destroy./code>/pre>p>这是一个全部资源销毁命令,执行后code>tf/code>文件配置的所有资源都认为是要销毁的/p>p>写在最后:br />自动化运维远不止于一个Terraform,在实际应用中还需结合更多工具降低我们的运维成本,让运维更高效更加自动化,欢迎关注微信公众号:程序员到架构师,回复font color#0082ef>Terraform/font>获取更多内容,后续也将继续推送更多有关自动化运维的文章/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/518.html title腾讯云支持Terraform开发实践>腾讯云支持Terraform开发实践/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2018-03-15T17:41:29+08:00>2018.03.15/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/golang relcategory tag>Golang/a> a hrefhttps://979137.com/archives/category/cloud relcategory tag>云计算/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/golang reltag>Golang/a>a hrefhttps://979137.com/archives/tag/provider reltag>Provider/a>a hrefhttps://979137.com/archives/tag/terraform reltag>Terraform/a>a hrefhttps://979137.com/archives/tag/infrastructure reltag>基础架构/a>a hrefhttps://979137.com/archives/tag/tencent_cloud reltag>腾讯云/a>/li> /ul> /div> div classpost_content clearfix> blockquote>p> Terraform是国际著名的开源的资源编排工具,据不完全统计,全球已有超过一百家云厂商及服务提供商支持Terraform。这篇文章从Terraform-Provider系统架构开始,到Terraform核心库讲解,到实践Terraform-Provider开发,再到单元测试,比较完整的描述了支持Terraform的开发全过程/p>/blockquote>h2>1. Terraform是什么?/h2>p>a hrefhttps://www.terraform.io/>Terraform/a>是一款基于a href//979137.com/archives/category/golang>Golang/a>的开源的资源编排工具,可以让用户管理配置任何基础架构,可以管理公有云和私有云服务的基础架构,也可以管理外部服务。/p>p>如果你不知道什么叫资源编排,那 a hrefhttps://console.aws.amazon.com>AWS控制台/a> 、a hrefhttps://console.cloud.tencent.com>腾讯云控制台/a> 你一定知道,你可以在这些控制台管理你的所有云资源,Terraform和控制台作用一样,本质都是管理你的云资源,只不过,控制台是界面化的操作,而Terraform是通过配置文件来实现br />img srchttps://979137.com/wp-content/uploads/2018/04/Terraform.png alt />/p>blockquote>p> 当你的基础架构很复杂时,当你在某云厂商采买了规模较大的云资源或云服务时,当你的基础架构是基于混合云时,...,控制台的界面化操作,也许并不是最佳的管理工具,这时候,Terraform可能就是上古神器了/p>/blockquote>h2>2. 怎么使用Terraform管理基础架构?/h2>p>在开始开发之前,我们先了解下用户是怎么玩的,这尤其重要,这有助于更好的理解我们后续的开发流程和开发思路br />简单来说,用户就是维护一些类似 code>json/code> 格式的 code>.tf/code> 配置文件,通过对配置的增删改查,实现对基础架构资源的增删改查。/p>p>我在文章《a hrefhttps://979137.com/archives/870.html title巧用Terraform完成腾讯云上自动运维>巧用Terraform完成腾讯云上自动运维/a>》是完全站在用户角度,讲述如何利用Terraform管理基础架构的,这里不再重复用户层的内容/p>h2>3. 配置开发环境/h2>p>Terraform支持插件模型,并且所有 code>provider/code> 实际就是插件,插件以Go二进制文件的形式分发。虽然技术上可以用另一种语言编写插件,但几乎所有的Terraform插件都是用a hrefhttp://tapd.oa.com/yundesign/markdown_wikis/#1010045391006855097>Golang/a>编写的。/p>p>本文是在下列版本开发和测试的br />- Terraform 0.11.xbr />- Go 1.9 (to build the provider plugin)/p>p>为了不使本文篇幅太长,环境相关请直接参考我们 code>Github/code> 上的 a hrefhttps://github.com/tencentyun/terraform-provider-tencentcloud/blob/master/README.md>README.md/a>,这里就不重复写了,假设你已经准备好了开发环境/p>h2>4. Provider架构/h2>p>按照Go的开发习惯和Github路径,我把开发目录放在了/p>pre classline-numbers prism-highlight data-start1>code classlanguage-null>cd $GOPATH/src/github.com/tencentyun/terraform-provider-tencentcloud/code>/pre>p>接下来,我们了解下 code>tencentcloud/code> 的插件目录,以此了解 code>Provider/code> 架构/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>├─terraform-provider-tencentcloud 根目录│ ├─main.go 程序入口文件│ ├─AUTHORS 作者信息│ ├─CHANGELOG.md 变更日志│ ├─LICENSE 授权信息│ ├─debug.tf.example 调试配置文件示例│ ├─examples 示例配置文件目录│ │ ├─tencentcloud-eip EIP示例tf文件│ │ ├─tencentcloud-instance CVM示例tf文件│ │ ├─tencentcloud-nat NAT网关示例tf文件│ │ ├─tencentcloud-vpc VPC示例tf文件│ │ └─ ... 更多examples目录│ ├─tencentcloud Provider核心目录│ │ ├─basic_test.go 基础单元测试│ │ ├─config.go 公共配置文件│ │ ├─data_source_tc_availability_zones.go 可用区查询│ │ ├─data_source_tc_availability_zones_test.go│ │ ├─data_source_tc_nats.go NAT网关列表查询│ │ ├─data_source_tc_nats_test.go│ │ ├─data_source_tc_vpc.go VPC查询│ │ ├─data_source_tc_vpc_test.go│ │ ├─... 更多Data Source│ │ ├─helper.go 一些公共函数│ │ ├─provider.go Provider核心文件│ │ ├─provider_test.go│ │ ├─resource_tc_eip.go EIP资源管理程序│ │ ├─resource_tc_eip_test.go│ │ ├─resource_tc_instance.go CVM实例资源管理程序│ │ ├─resource_tc_instance_test.go│ │ ├─resource_tc_nat_gateway.go NAT网关资源管理程序│ │ ├─resource_tc_nat_gateway_test.go│ │ ├─resource_tc_vpc.go VPC网关资源管理程序│ │ ├─resource_tc_vpc_test.go│ │ ├─... 更多资源管理程序│ │ ├─service_eip.go 封装的EIP相关Service│ │ ├─service_instance.go 封装的CVM实例相关Service│ │ ├─service_vpc.go 封装的VPC相关Service│ │ ├─...│ │ ├─validators.go 公共的参数校验函数│ ├─vendor 依赖的第三方库│ ├─website Web相关文件│ │ ├─tencentcloud.erb 文档左侧菜单栏│ │ ├─docs 文档markdown源文件目录│ │ │ ├─d data相关文档(data_source_*)│ │ │ │ ├─availability_zones.html.md│ │ │ │ ├─nats.html.markdown│ │ │ │ ├─vpc.html.markdown│ │ │ │ ├─...│ │ │ ├─index.html.markdown│ │ │ ├─r resource相关文档(resource_*)│ │ │ │ ├─instance.html.markdown│ │ │ │ ├─nat_gateway.html.markdown│ │ │ │ ├─vpc.html.markdown│ │ │ │ └─.../code>/pre>p>/p>p>结构主要分五部分/p>ul>li>code>main.go/code>,插件入口/li>li>examples,示例目录,因为你的插件最终是给用户用的,一个比较理想的示例,是用户拉到代码后,可以直接跑起来/li>li>tencentcloud,最重要的目录,也就是我们的插件目录,里面都是Go文件,其中ul>li>code>provider.go/code> 这是插件的根源,用于描述插件的属性,如:配置的秘钥,支持的资源列表,回调配置等/li>li>code>data_source_*.go/code> 定义的一些用于读调用的资源,主要是查询接口/li>li>code>resource_*.go/code> 定义的一些写调用的资源,包含资源增删改查接口/li>li>code>service_*.go/code> 按资源大类划分的一些公共方法/li>/ul>/li>li>vendor,依赖的第三方库/li>li>website,文档,重要性同examples/li>/ul>h2>5. 生命周期/h2>p>下图是Terraform的整个执行过程:br />img srchttps://979137.com/wp-content/uploads/2018/04/terraform-provider.png alt />/p>ul>li>① ~ ④ 是在寻找 code>Provider/code>,code>tencentcloud/code> 插件就是这时候加载的/li>li>⑤ 是读取用户的配置文件,通过配置文件,可以获得分别属于哪种资源,以及每个资源的状态/li>li>⑥ 根据资源的状态,调用不同的函数,code>Create/code> code>Update/code> code>Delete/code> 都属于写操作,而 code>Read/code> 操作,只在 code>Update/code> 的时候,作为前置操作p>何谓 code>Create/code> ?br />当在 code>.tf/code> 文件增加一个新的资源配置时,这时候 Terraform 认为是 code>Create/code>/p>p>何谓 code>Update/code> ?br />当在 code>.tf/code> 文件针对已经创建好的资源,修改其中一个或多个参数时,这时候 Terraform 认为是 code>Update/code>/p>p>何谓 code>Delete/code> ?br />当把 code>.tf/code> 文件中已经创建好的资源配置删掉后,或执行 code>terraform destroy/code> 命令时,这时候 Terraform 认为是 code>Delete/code>/p>p>何谓 code>Read/code> ?br />顾名思义,这是一个查询资源的操作,如前述 code>Read/code> 只在 code>Update/code> 的时候,作为前置操作,实际作用就是检查资源是否存在,以及更新资源属性到本地/p>/li>/ul>blockquote>p>细心的你一定注意到了 a hrefhttps://github.com/zqfan/tencentcloud-sdk-go>tencentcloud-sdk-go/a> 这个 code>package/code>,a hrefhttps://github.com/zqfan/tencentcloud-sdk-go>tencentcloud-sdk-go/a> 是我们封装的一个独立于 Terraform 之外的基于 a hrefhttps://cloud.tencent.com/document/api>Tencent Cloud API/a> 的Go版SDK/p>p> 其作用就是负责调用 a hrefhttps://cloud.tencent.com/document/api>Tencent Cloud API/a>/p>p> 当然,你也可以不用它,直接在你的 code>terraform-provider/code> 里组装参数、发送请求,但我们不建议这么做,使用SDK方式,可以让你的代码更加优雅,可以实现对出入参、HTTP请求的集中管理,可以让你的常用接口更好的复用,减少代码冗余/p>/blockquote>h2>6. 定义资源/h2>p>Terraform官网有个从 code>main.go/code> 入口开始编写自定义Provider的指引 a hrefhttps://www.terraform.io/guides/writing-custom-terraform-providers.html>Writing Custom Providers/a>,建议先浏览一遍。/p>p>成为Terraform提供商(开发Terraform插件),实际是对上游 code>API/code> 的抽象,而所谓的资源就是我们的服务,比如云主机、私有网络、NAT网关。按惯例,我们要把每个资源放在自己的插件目录下,并以资源命名,前缀为 code>resource_/code> 或 code>data_source_/code>,比如/p>blockquote>p> tencentcloud/resource_tc_nat_gateway.go/p>/blockquote>pre classline-numbers prism-highlight data-start1>code classlanguage-go>package tencentcloudimport ( github.com/hashicorp/terraform/helper/schema)func resourceTencentCloudNatGateway() *schema.Resource { return &schema.Resource{ Create: resourceTencentCloudNatGatewayCreate, Read: resourceTencentCloudNatGatewayRead, Update: resourceTencentCloudNatGatewayUpdate, Delete: resourceTencentCloudNatGatewayDelete, Schema: mapstring*schema.Schema{ vpc_id: &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, name: &schema.Schema{ Type: schema.TypeString, Required: true, ValidateFunc: validateStringLengthInRange(1, 60), }, max_concurrent: &schema.Schema{ Type: schema.TypeInt, Required: true, }, bandwidth: &schema.Schema{ Type: schema.TypeInt, Required: true, }, assigned_eip_set: &schema.Schema{ Type: schema.TypeSet, Required: true, Elem: &schema.Schema{ Type: schema.TypeString, }, MinItems: 1, MaxItems: 10, }, }, }}func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { return nil}func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error { return nil}func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error { return nil}func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error { return nil}/code>/pre>p>这里实际就是返回了一个 a hrefhttps://godoc.org/github.com/hashicorp/terraform/helper/schema#Resource>schema.Resource/a> 类型的结构体,结构体中我们定义了资源参数和CRUD操作/p>ul>li>Create/li>li>Read/li>li>Update/li>li>Delete/li>li>Schema/li>/ul>p>其中 code>Schema/code> 就是定义的资源参数,是 code>mapstring*schema.Schema/code> 类型的嵌套数组,这是一个非常重要的数组,在Terraform里,你也理解为这些就是一个资源的属性br />在我们本次的示例中,就是一个NAT网关的所有属性(这些属性,我们可以在a hrefhttps://cloud.tencent.com/document/api/215/4094>NAT网关的云API/a>中看到)/p>p>每个属性,它的值都是一个结构体,包含了若干属性,这些属性,都是围绕资源属性值的,下面逐一介绍/p>h4>Type em>schema.ValueType/em>/h4>p>定义这个属性的值的数据类型,可选值及对应的数据类型/p>ul>li>TypeBool - em>bool/em>/li>li>TypeInt - em>int/em> /li>li>TypeFloat - em>float64/em> /li>li>TypeString - em>string/em> /li>li>TypeList - em>interface{}/em> /li>li>TypeMap - em>mapstringinterface{}/em> /li>li>TypeSet - em>*schema.Set/em> /li>/ul>h4>Required em>bool/em>/h4>p>也就我们经常在 code>API/code> 里说的 参数是否必填,默认 code>false/code>,当设置为 code>true/code> 后,用户对资源增删改操作时,都需要配置该参数/p>h4>Optional em>bool/em>/h4>p>是否可选的,和 code>Required/code> 互斥的,不能同时配置 code>Required/code> 和 code>Optional/code>,即一个属性(参数)要么必填,要么可选/p>h4>ForceNew em>bool/em>/h4>p>如果设置为 code>true/code>,当资源属性值发生变化时,不会触发修改动作,而是删除该资源,再创建新的资源,即:/p>blockquote>p> 修改 删除 + 创建/p>/blockquote>p>这是一个非常有用的属性,我们很多云资源的很多属性都不支持修改,比如/p>ul>li>一个CVM实例创建时指定的子网,创建后,是不支持修改的/li>li>一个NAT网关创建时指定的VPC,创建后,是无法修改的/li>/ul>p>在控制台可以通过前端技术实现这样的限制,Terraform 同样可以做到这样的限制,但 code>ForceNew/code> 实现了更高级的用法,给用户提供了更多选择,/p>blockquote>p> 一个有趣的事情,如果某种云资源的所有属性,都是code>Required/code>,并且属性联合起来,具有唯一性,比如路由表的路由策略、DNAT规则、KeyPair、...,都是这类特性,这时候你修改一个属性,实际就等价于删除旧资源,创建新资源br /> 这时候,你就可以把所有属性的code>ForceNew/code> 设为 code>true/code>,然后不用实现 code>Update/code> 函数了,因为无论用户修改哪个属性,都是走 code>Delete/code> - code>Create/code> 的流程,根本不会走到 code>Update/code> 的流程里,但实现的效果,都是一样的,用户是无感知的/p>/blockquote>h4>ValidateFunc em>SchemaValidateFunc/em>/h4>p>属性值的扩展验证函数,验证IP合法性示例:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>func validateIp(v interface{}, k string) (ws string, errors error) { value : v.(string) ip : net.ParseIP(value) if ip nil { errors append(errors, fmt.Errorf(%q must contain a valid IP, k)) } return}/code>/pre>p>/p>h4>MinItems、MaxItems em>int/em>/h4>p>当 code>Type/code> 为 code>TypeSet/code> 或 code>TypeList/code> 类型时,可以给 code>MinItems/code> 和 code>MaxItems/code> 赋值,限定属性值元素的最小个数和最大个数,上述代码中,我们限定了NAT网关的关联EIP个数范围是1~10个/p>h3>CRUD操作/h3>p>这4个操作 code>Create/code> code>Read/code> code>Update/code> code>Delete/code>,指向的是4个函数,也是我们重点要实现的。br />在生命周期一节中,我们知道了Terraform是根据资源的模式和状态,来决定是否需要创建新资源,更新现有资源或销毁资源的,而最终就是调用这4个函数来实现的/p>h2>7. CRUD实现/h2>p>了解了用户行为、Terraform执行流程、资源管理逻辑,现在就是实现这些功能的时候了br />因为这块内容较多,这里继续用NAT网关作为示例,详述一个资源CURD的实现/p>p>开始之前,我们需要引入更多的包,都是我们后面要用到的/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>import ( encoding/json errors fmt log github.com/hashicorp/terraform/helper/schema github.com/zqfan/tencentcloud-sdk-go/common vpc github.com/zqfan/tencentcloud-sdk-go/services/vpc/unversioned)//...func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { return nil}func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error { return nil}func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error { return nil}func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error { return nil}/code>/pre>p>上述代码中,我们看到,我们要实现的资源管理函数,出参都是 code>error/code> 类型,说明Terraform都是根据 code>error/code> 来判断成功与否的,返回 code>nil/code> 时表示操作成功,否则就报错/p>p>入参都是 code>*schema.ResourceData/code> 类型的参数 code>d/code>,和 code>interface{}/code> 类型的参数 code>meta/code>,具体这两个参数有什么用呢?br />这是我们这节的关键!br />参数 code>d/code> 是我们开发过程中用的最多的参数,它的数据类型是个对象,包含了非常的方法,下面我们介绍几个常用的方法/p>h4>func (*ResourceData) Get/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) Get(key string) interface{}/code>/pre>p>用来获取给定 code>Key/code> 的数据,如果给定的 code>Key/code> 不存在,会返回 code>nil/code>br />通过 code>Set/code> 方法设置的数据,以及用户配置的参数,都可以通过这个方法获得br />一般,我们在 code>Create/code> 资源的时候,用的比较多/p>h4>func (*ResourceData) GetOk/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) GetOk(key string) (interface{}, bool)/code>/pre>p>检查给定的 code>Key/code> 是否设置为一个非0的值,一般我们在获取 code>Optional/code> 类型的属性值的时候,会用到/p>h4>func (*ResourceData) SetId/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) SetId(v string)/code>/pre>p>Terraform对资源的管理都是围绕ID实现的,每个资源都有一个唯一ID,一个ID代表一个资源,因此,当创建资源后,需要调用这个方法写入资源ID,一般服务端都会返回资源唯一ID,比如我们的示例中,这个ID就是NAT网关的ID.br />eg: nat-79r5e43i/p>p>这时候,你是不是有一个疑惑?我们的资源没有唯一 code>ID/code> 怎么办?/p>p>对于没有唯一ID的资源,比如路由策略、安全组规则的增删改查,我们就需要自己构造ID了。br />可以用某个参数作为ID;也可以多个参数联合起来;也可以自己实现一个算法生成ID。br />前提条件就是一定要strong>唯一/strong> ,然后我们在用到ID的时候,再反解出来,这就间接实现了我们所需要的唯一 code>ID/code>/p>h4>func (*ResourceData) Id/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) Id() string/code>/pre>p>获取当前的资源ID,也就是 code>SetId/code> 方法写入的值,比如我们在 code>Read/code> code>Update/code> code>Delete/code> 的时候,都需要用到ID,映射到对应的资源,从而完成对某个资源的读取,修改,删除/p>h4>func (*ResourceData) Set/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) Set(key string, value interface{}) error/code>/pre>p>给某个 code>Key/code> 设置值,设置后,可以用 code>Get/code> 方法获取,一般用于 code>Read/code> 操作,从服务端 code>Read/code> 完数据后,会将资源的属性 code>Set/code> 到本地,用于后续的其他资源管理操作/p>h4>func (*ResourceData) HasChange/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) HasChange(key string) bool/code>/pre>p>想象一下,当用户修改了他的配置文件(也就是修改资源的属性),我们的程序是怎么知道的?br />这时候,就需要用到 code>HasChange/code> 了,检查给定的 code>Key/code> 是否发生变化,一个非常有用而且经常会用到的方法,一般在 code>Update/code> 操作的时候,我们需要监控用户的配置文件,发生变化时,我们就触发变更操作/p>h4>func (*ResourceData) GetChange/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) GetChange(key string) (interface{}, interface{})/code>/pre>p>这个方法就是当我们在使用 code>HasChange/code> 方法知道数据发生变化时,用这个方法可以获取到变化前后的数据,即旧数据和新数据br />比如用户修改了NAT网关的关联弹性IP,这时候,我们就需要将对比新旧数据,将用户删减的弹性IP,从服务端解绑,用户增加的弹性IP,绑定到NAT网关/p>h4>func (*ResourceData) Partial/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) Partial(on bool)/code>/pre>p>一般我们的资源属性,有非常多属性是支持修改的,比如我们这次示例中NAT网关,其中NAT网关的名称 code>name/code>、最大并发连接数 code>max_concurrent/code>、带宽上限 code>bandwidth/code>、关联弹性IP code>assigned_eip_set/code> 都是支持修改的。/p>p>对用户来说,这些都是NAT网关的属性值而已,但对我们开发人员来说,涉及到的后端接口却是不一样的,这时候,如果用户修改了多个属性值,按照文档流的执行方式,如果前面执行的修改成功了,后面执行的失败了,这时候如果退出程序,给用户报错,就不合理了,因为实际我们的后端,已经修改了其中部分属性值。br />这时候,服务端的数据和用户本地的数据,也不一致了,后续的其他操作,也会出现比较严重的问题/p>p>所以,我们应该不难理解这个方法的用途,就是用来设置是否 code>允许修改部分属性/code> 的方法,默认code>false/code>,当开启 code>允许修改部分属性/code> 后,使用了 code>SetPartial/code> 方法设置的属性,即便 code>Update/code> 出现错误,已经修改成功的属性,也会将状态同步到本地,程序下次执行时,就不会认为是要更新的了/p>p>总结三个字就是 “非事务”/p>h4>func (*ResourceData) SetPartial/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-null>func (d *ResourceData) SetPartial(k string)/code>/pre>p>这个方法就是配合 code>Partial/code> 方法使用的,经过这个方法设置的属性,code>允许修改部分属性/code> 的逻辑才有效/p>h3>7.1 创建资源/h3>p>这里就是创建NAT网关/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { // 创建请求对象 args : vpc.NewCreateNatGatewayRequest() // 给对象属性赋值,这里要注意,因为 args.VpcId common.StringPtr(d.Get(vpc_id).(string)) args.NatName common.StringPtr(d.Get(name).(string)) // 因为 max_concurrent 和 bandwidth 是可选值,所以我们用 GetOk 判断用户是否配置 if v, ok : d.GetOk(max_concurrent); ok { args.MaxConcurrent common.IntPtr(v.(int)) } if v, ok : d.GetOk(bandwidth); ok { args.Bandwidth common.IntPtr(v.(int)) } // assigned_eip_set 是个数组,取值方法和整型、字符串有点不一样,需要用 List 方法 eips : d.Get(assigned_eip_set).(*schema.Set).List() args.AssignedEipSet common.StringPtrs(expandStringList(eips)) // 这里就是发送请求了 client : meta.(*TencentCloudClient) conn : client.vpcConn response, err : conn.CreateNatGateway(args) b, _ : json.Marshal(response) log.Printf(DEBUG conn.CreateNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.CreateNatGateway error: %v, err) } // 因为NAT网关的创建是异步的,到这里,我们只拿到了一个BillId,所以需要用到轮询逻辑了 if _, err : client.PollingVpcBillResult(response.BillId); err ! nil { return err } // 为了方便调试,我们把NAT网关ID记录到日志 log.Printf(DEBUG conn.CreateNatGateway NatGatewayId: %s, *response.NatGatewayId) // 调用 SetId 写入资源ID(这里就是NAT网关ID),关于 SetId 方法的作用,参考前面说的 d.SetId(*response.NatGatewayId) return nil}/code>/pre>p>上述代码中 code>PollingVpcBillResult/code>,我们说到了轮询,其实在Terraform开发中,轮询这个操作,是用的很频繁的,主要适用于异步的服务端接口,比如当前示例的NAT网关创建,还有后面会讲到的修改带宽,又如一些资源删除也都是异步的。br />服务端只返回一个任务ID,这时候需要我们在客户端轮询任务,直到结果返回,我们才能直到这个资源的真正的状态!/p>p>这个方法位于 code>service_vpc.go/code>,并且是作为 code>*TencentCloudClient/code> 对象的一个方法,核心是用到了Terraform官方的 a hrefhttps://github.com/hashicorp/terraform/tree/master/helper/resource>resource/a> 库,直接来看下这个方法吧,/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>func (client *TencentCloudClient) PollingVpcBillResult(billId *string) (status bool, err error) { queryReq : vpc.NewQueryNatGatewayProductionStatusRequest() queryReq.BillId billId status false // 设置超时时间为3分钟 err resource.Retry(3*time.Minute, func() *resource.RetryError { queryResp, err : client.vpcConn.QueryNatGatewayProductionStatus(queryReq) b, _ : json.Marshal(queryResp) log.Printf(DEBUG client.vpcConn.QueryNatGatewayProductionStatus response: %s, b) if _, ok : err.(*common.APIError); ok { // 返回 NonRetryableError 错误,resource 会退出重试,并返回错误信息 return resource.NonRetryableError(fmt.Errorf(client.vpcConn.QueryNatGatewayProductionStatus error: %v, err)) } // 返回 nil 之后,表示操作成功,resource 就会退出重试 if *queryResp.Data.Status vpc.BillStatusSuccess { return nil } // 返回一个 RetryableError 错误,resource 将持续重试 return resource.RetryableError(fmt.Errorf(billId %v, not ready, status: %v, billId, *queryResp.Data.Status)) }) return}/code>/pre>p>/p>h3>7.2 读取资源/h3>p>在 code>Create/code> 的代码末尾,我们看到了 code>SetId/code>,而 code>Read/code> 操作,我们就是要根据资源ID,查询资源,然后调用 code>Set/code> 方法回写本地/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error { conn : meta.(*TencentCloudClient).vpcConn descReq : vpc.NewDescribeNatGatewayRequest() descReq.NatId common.StringPtr(d.Id()) descResp, err : conn.DescribeNatGateway(descReq) b, _ : json.Marshal(descResp) log.Printf(DEBUG conn.DescribeNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.DescribeNatGateway error: %v, err) } // 未找到资源时,为什么不报错?SetId() 又是什么意思? if *descResp.TotalCount 0 || len(descResp.Data) 0 { d.SetId() return nil } else if err ! nil { return err } nat : descResp.Data0 d.Set(name, *nat.NatName) d.Set(max_concurrent, *nat.MaxConcurrent) d.Set(bandwidth, *nat.Bandwidth) d.Set(assigned_eip_set, nat.EipSet) return nil}/code>/pre>p>我们在代码15行,留了个疑问,这也是很多开发,初次开发Terraform时,不太理解的地方!/p>p>当从服务端查询没有数据时,我们并不直接报错,而是把ID置空,并且返回 code>nil/code>,这样做的目的是因为我们的云资源管理行为,不只在Terraform,还有控制台,也可能基于云API的其他工具,倘若不是因为你的代码Bug导致查询失败而未找到数据,那就是在其他工具删除了该资源导致资源为找到,这时候/p>ul>li>返回 code>nil/code>,是为了不让程序退出,让程序不认为这是错误/li>li>把ID置空,是为了改变资源状态,前面我们提到Terraform,对于资源的管理,是完全基于ID的,当我们把ID置空,Terraform未找到资源ID,就会认为这是一个新资源,这也是我们所预期的/li>/ul>h3>7.3 修改资源/h3>p>我们在生命周期那一节,讲到了 code>Update/code> 操作前,Terraform实际会先调用 code>Read/code>,为什么呢?br />因为Terraform判断一个资源状态,是依据本地的 code>terraform.tfstate/code> 文件,这里记录所有配置(即资源)的状态,但是状态并非实时的,所以 Terraform 在做 code>Update/code> 操作之前,会先从服务器 code>Read/code> 数据,用最新的数据和本地做对比,获取最新的资源状态/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error { client : meta.(*TencentCloudClient) conn : client.vpcConn // 开启 允许部分属性修改 功能 d.Partial(true) // 标识是否有修改 attributeUpdate : false updateReq : vpc.NewModifyNatGatewayRequest() updateReq.VpcId common.StringPtr(d.Get(vpc_id).(string)) updateReq.NatId common.StringPtr(d.Id()) // 修改NAT网关名称 if d.HasChange(name) { d.SetPartial(name) var name string if v, ok : d.GetOk(name); ok { name v.(string) } else { return fmt.Errorf(cannt change name to empty string) } updateReq.NatName common.StringPtr(name) attributeUpdate true } // 修改带宽上限 if d.HasChange(bandwidth) { d.SetPartial(bandwidth) var bandwidth int if v, ok : d.GetOk(bandwidth); ok { bandwidth v.(int) } else { return fmt.Errorf(cannt change bandwidth to empty string) } updateReq.Bandwidth common.IntPtr(bandwidth) attributeUpdate true } // 修改名称和带宽上限,用的同一个接口,如果有修改,就提交 if attributeUpdate { updateResp, err : conn.ModifyNatGateway(updateReq) b, _ : json.Marshal(updateResp) log.Printf(DEBUG conn.ModifyNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.ModifyNatGateway error: %v, err) } } // 修改并发连接数上限,这里用到了 GetChange,对比新旧数据 // 因为我们的NAT网关的并发连接数上限,只能升不能降 if d.HasChange(max_concurrent) { d.SetPartial(max_concurrent) old_mc, new_mc : d.GetChange(max_concurrent) old_max_concurrent : old_mc.(int) new_max_concurrent : new_mc.(int) if new_max_concurrent < old_max_concurrent { return fmt.Errorf(max_concurrent only supports upgrade) } upgradeReq : vpc.NewUpgradeNatGatewayRequest() upgradeReq.VpcId updateReq.VpcId upgradeReq.NatId updateReq.NatId upgradeReq.MaxConcurrent common.IntPtr(new_max_concurrent) upgradeResp, err : conn.UpgradeNatGateway(upgradeReq) b, _ : json.Marshal(upgradeResp) log.Printf(DEBUG conn.UpgradeNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.UpgradeNatGateway error: %v, err) } if _, err : client.PollingVpcBillResult(upgradeResp.BillId); err ! nil { return err } } // 修改关联弹性EIP,这块逻辑稍微复杂点,因为 `assigned_eip_set` 是个数组 // 我们需要对比新旧数据,拿到用户删除的数组元素和增加的数组元素 // 然后调用解绑接口,解绑用户删除的数组元素;再调用绑定接口,绑定用户增加的数组元素 if d.HasChange(assigned_eip_set) { o, n : d.GetChange(assigned_eip_set) os : o.(*schema.Set) ns : n.(*schema.Set) old_eip_set : os.List() new_eip_set : ns.List() if len(old_eip_set) > 0 && len(new_eip_set) > 0 { // Unassign old EIP unassignIps : os.Difference(ns) if unassignIps.Len() ! 0 { unbindReq : vpc.NewEipUnBindNatGatewayRequest() unbindReq.VpcId updateReq.VpcId unbindReq.NatId updateReq.NatId unbindReq.AssignedEipSet common.StringPtrs(expandStringList(unassignIps.List())) unbindResp, err : conn.EipUnBindNatGateway(unbindReq) b, _ : json.Marshal(unbindResp) log.Printf(DEBUG conn.EipUnBindNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.EipUnBindNatGateway error: %v, err) } if _, err : client.PollingVpcTaskResult(unbindResp.TaskId); err ! nil { return err } } // Assign new EIP assignIps : ns.Difference(os) if assignIps.Len() ! 0 { bindReq : vpc.NewEipBindNatGatewayRequest() bindReq.VpcId updateReq.VpcId bindReq.NatId updateReq.NatId bindReq.AssignedEipSet common.StringPtrs(expandStringList(assignIps.List())) bindResp, err : conn.EipBindNatGateway(bindReq) b, _ : json.Marshal(bindResp) log.Printf(DEBUG conn.EipBindNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.EipBindNatGateway error: %v, err) } if _, err : client.PollingVpcTaskResult(bindResp.TaskId); err ! nil { return err } } } else { return errEipUnassigned } d.SetPartial(assigned_eip_set) } // 关闭 允许部分属性修改 功能 d.Partial(false) return nil}/code>/pre>p>/p>p>主要思路,概括下就是:br />1. 调用 code>Partial/code> 方法开启 em>允许部分属性修改/em> 功能br />2. 调用 code>HasChange/code> 方法检查是否变化,br />3. 调用 code>SetPartial/code> 方法把该属性加入到部分属性修改的集合里br />4. 调用 code>GetChange/code> 方法获取新旧数据(也可以直接 code>Get/code> 最新数据)br />5. 提交修改br />6. 调用 code>Partial/code> 方法关闭 em>允许部分属性修改/em> 功能/p>h3>7.4 删除资源/h3>p>删除资源就是根据资源ID,从服务端将对应的资源删除/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error { client : meta.(*TencentCloudClient) deleteReq : vpc.NewDeleteNatGatewayRequest() deleteReq.VpcId common.StringPtr(d.Get(vpc_id).(string)) deleteReq.NatId common.StringPtr(d.Id()) deleteResp, err : client.vpcConn.DeleteNatGateway(deleteReq) b, _ : json.Marshal(deleteResp) log.Printf(DEBUG client.vpcConn.DeleteNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(ERROR client.vpcConn.DeleteNatGateway error: %v, err) } _, err client.PollingVpcTaskResult(deleteResp.TaskId) return err}/code>/pre>blockquote>p> 示例是一个最简单的删除操作,在实际应用中,如果你的资源删除是异步的,或者删除操作,还依赖其他资源删除,比如当删除一个私有网络资源时,如果网络内还有其他资源,比如子网、VPN等,调用删除接口时,会报错,导致删除失败!br /> 遇到这些场景,我们还需要用到前面提到的重试操作,br /> 就是当删除失败,特定原因下(一般就是有依赖关系)我们要执行重试,因为Terraform在删除资源时,是有次序的,直接删除有可能删不掉,而重试,当依赖关系都删完后,就能删除最顶层的被依赖的资源了/p>/blockquote>p>至此,一个基本的资源管理程序就算写完了!最后你还需要将资源管理函数配置到 code>provider.go/code> 的 code>ResourcesMap/code> 映射关系列表中,才能真正被使用/p>h2>8. 编写单元测试用例/h2>p>到了测试环节,你可以自己编写 code>tf/code> 文件,编译插件/p>pre classline-numbers prism-highlight data-start1>code classlanguage-null>go build -o terraform-provider-tencentcloud/code>/pre>p>然后测试你的程序/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>terrform planterrform apply/code>/pre>p>但我们非常不鼓励你这么做,我们强烈建议你自己编写单元测试用例,测试你的程序,在前面的 strong>Provider架构/strong> 章节中,你可以看到许多的 code>*_test.go/code> 这就是我们的单元测试用例br />如果要成为Terraform官方认证的code>provider/code>,单元测试用例,也是必不可少的/p>p>我们先来看下Terraform的单元测试系统流程图br />img srchttps://979137.com/wp-content/uploads/2018/04/test_acc.png alt />br />下面是NAT网关资源管理程序的单元测试用例:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-go>package tencentcloudimport ( encoding/json fmt log testing github.com/hashicorp/terraform/helper/resource github.com/hashicorp/terraform/terraform github.com/zqfan/tencentcloud-sdk-go/common vpc github.com/zqfan/tencentcloud-sdk-go/services/vpc/unversioned)func TestAccTencentCloudNatGateway_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, // 配置 资源销毁结果检查函数 CheckDestroy: testAccCheckNatGatewayDestroy, // 配置 测试步骤 Steps: resource.TestStep{ { // 配置 配置内容 Config: testAccNatGatewayConfig, // 配置 验证函数 Check: resource.ComposeTestCheckFunc( // 验证资源ID testAccCheckTencentCloudDataSourceID(tencentcloud_nat_gateway.my_nat), // 验证资源属性,能匹配到,肯定就是创建成功了 resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, name, terraform_test), resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, max_concurrent, 3000000), resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, bandwidth, 500), resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, assigned_eip_set.#, 2), ), }, { // 配置 配置内容 Config: testAccNatGatewayConfigUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckTencentCloudDataSourceID(tencentcloud_nat_gateway.my_nat), // 验证修改后的属性值,如果能匹配到,肯定就是修改成功了 resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, name, new_name), resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, max_concurrent, 10000000), resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, bandwidth, 1000), resource.TestCheckResourceAttr(tencentcloud_nat_gateway.my_nat, assigned_eip_set.#, 2), ), }, }, })}// testAccProviders 在测试前会根据 Config 建立测试资源,测试结束后又会全部销毁// 这个函数就是检查资源是否销毁用的,代码逻辑比较好理解,就是根据ID查询资源是否存在func testAccCheckNatGatewayDestroy(s *terraform.State) error { conn : testAccProvider.Meta().(*TencentCloudClient).vpcConn // 这用到了 s.RootModule().Resources 数组 // 这个数组的属性反应的就是资源状态文件 terraform.tfstate for _, rs : range s.RootModule().Resources { if rs.Type ! tencentcloud_nat_gateway { continue } descReq : vpc.NewDescribeNatGatewayRequest() descReq.NatId common.StringPtr(rs.Primary.ID) descResp, err : conn.DescribeNatGateway(descReq) b, _ : json.Marshal(descResp) log.Printf(DEBUG conn.DescribeNatGateway response: %s, b) if _, ok : err.(*common.APIError); ok { return fmt.Errorf(conn.DescribeNatGateway error: %v, err) } else if *descResp.TotalCount ! 0 { return fmt.Errorf(NAT Gateway still exists.) } } return nil}// 基本用法配置文件,机智的你,一定发现了,这不是和debug用的tf文件一样一样的么const testAccNatGatewayConfig `resource tencentcloud_vpc main { name terraform test cidr_block 10.6.0.0/16}resource tencentcloud_eip eip_dev_dnat { name terraform_test}resource tencentcloud_eip eip_test_dnat { name terraform_test}resource tencentcloud_nat_gateway my_nat { vpc_id ${tencentcloud_vpc.main.id} name terraform_test max_concurrent 3000000 bandwidth 500 assigned_eip_set ${tencentcloud_eip.eip_dev_dnat.public_ip}, ${tencentcloud_eip.eip_test_dnat.public_ip}, }`// 修改用法配置文件,机智的你一定发现了,这不就是和debug修改后的tf文件一样一样的么const testAccNatGatewayConfigUpdate `resource tencentcloud_vpc main { name terraform test cidr_block 10.6.0.0/16}resource tencentcloud_eip eip_dev_dnat { name terraform_test}resource tencentcloud_eip eip_test_dnat { name terraform_test}resource tencentcloud_eip new_eip { name terraform_test}resource tencentcloud_nat_gateway my_nat { vpc_id ${tencentcloud_vpc.main.id} name new_name max_concurrent 10000000 bandwidth 1000 assigned_eip_set ${tencentcloud_eip.eip_dev_dnat.public_ip}, ${tencentcloud_eip.new_eip.public_ip}, }`/code>/pre>p>开始测试/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>export TF_ACCtruecd tencentcloudgo test -i; go test -test.run TestAccTencentCloudNatGateway_basic -v/code>/pre>p>我们可以看到,用官方的 code>testAccProviders/code>,除了自动编译,测试流程也更加标准化,全面覆盖 code>Create/code> code>Update/code> code>Delete/code>,针对同一个资源管理程序,你还可以编写很多更复杂的场景,加入到 code>Steps/code>,或者分成多个测试用例,这样的测试会更加全面!/p>p>写在最后:br />目前已经越来越多的云厂商和服务商支持Terraform,为用户提供服务,Terraform也日渐普遍。欢迎关注微信公众号:程序员到架构师,回复font color#0082ef>Terraform/font>获取更多内容,后续也将继续推送更多有关云计算的文章/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/169.html titleMySQL位运算的应用>MySQL位运算的应用/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2017-01-13T17:42:20+08:00>2017.01.13/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/db relcategory tag>数据库/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/mysql reltag>MySQL/a>a hrefhttps://979137.com/archives/tag/sql reltag>SQL/a>a hrefhttps://979137.com/archives/tag/algorithm reltag>算法/a>/li> /ul> /div> div classpost_content clearfix> blockquote>p>我们知道,PHP当中的错误级别常量,是根据二进制位特性而确定的一个个整数,可以简单的通过位运算定制PHP的错误报告。我们也可以将其应用到 MySQL 当中!/p>/blockquote>p>你是否遇到过下面这样的情况?/p>p>span stylefont-size: 14pt;>strong>一、用户可能有若干不同属性或不同状态,然后你可能会在数据表中通过一个个字段去标记和实现,比如:/strong>/span>/p>table>tbody>tr>td>id/td>td>int(11)/td>td>用户ID(自增)/td>/tr>tr>td>username/td>td>char(50)/td>td>用户名/td>/tr>tr>td>password/td>td>char(50)/td>td>密码/td>/tr>tr>td>.../td>td>.../td>td>.../td>/tr>tr>td>confirm_info/td>td>tinyint(1)/td>td>是否确认资料 0/1 /td>/tr>tr>td>confirm_contract/td>td>tinyint(1)/td>td>是否确认协议 0/1 /td>/tr>tr>td>freeze/td>td>tinyint(1)/td>td>冻结状态 0/1 /td>/tr>tr>td>payer/td>td>tinyint(1)/td>td>笔记是否付费用户 0/1 /td>/tr>tr>td>high_quality/td>td>tinyint(1)/td>td>标记是否优质用户 0/1 /td>/tr>/tbody>/table>p>span stylefont-size: 14pt;>strong>二、用户拥有多个用户组(角色),常见的实现方法有:/strong>/span>/p>p>1、增加一个字段,专门存它的用户组,用逗号隔开/p>table>tbody>tr>td>id/td>td>int(11)/td>td>用户ID(自增)/td>/tr>tr>td>username/td>td>char(50)/td>td>用户名/td>/tr>tr>td>password/td>td>char(50)/td>td>密码/td>/tr>tr>td>.../td>td>.../td>td>.../td>/tr>tr>td>confirm_info/td>td>tinyint(1)/td>td>是否确认资料 0/1 /td>/tr>tr>td>confirm_contract/td>td>tinyint(1)/td>td>是否确认协议 0/1 /td>/tr>tr>td>freeze/td>td>tinyint(1)/td>td>冻结状态 0/1 /td>/tr>tr>td>payer/td>td>tinyint(1)/td>td>笔记是否付费用户 0/1 /td>/tr>tr>td>high_quality/td>td>tinyint(1)/td>td>标记是否优质用户 0/1 /td>/tr>tr>td>span stylecolor: #3366ff;>role_id/span>/td>td>span stylecolor: #3366ff;>varchar(255)/span>/td>td>span stylecolor: #3366ff;>如:1,3,11/span>/td>/tr>/tbody>/table>p>使用 FIND_IN_SET() 或 LIKE 进行数据查询。这种存储方式,虽然比较直观,结构也比较接单,但是,但是查询和维护数据都不方便/p>p>2、增加一个关联表/p>table>tbody>tr>td>user_id/td>td>int(11)/td>td>用户ID/td>/tr>tr>td>role_id/td>td>int(11)/td>td>角色ID/td>/tr>tr>td>.../td>td>.../td>td>.../td>/tr>/tbody>/table>p>通过JOIN,连表查询。这种存储方式,似乎很好扩展,无论是增加了用户组,还是重新给用户划分用户组,直接操作这张关联表就行。但是,连表查询终究是需要操作多张表的,效率肯定不如单表查询,而且如果用户属性多了(不仅限于分组),那就得关联更多的表或者查多次,使查询变得更加复杂!/p>hr />p>我们知道,PHP当中的错误级别常量,是根据二进制位特性而确定的一个个整数,可以简单的通过位运算定制PHP的错误报告。我们也可以将其应用到 MySQL 当中,还是以上面的用户表为例,我们只用一字段 status,专门记录用户状态,其结构如下:/p>table>tbody>tr>td>id/td>td>int(11)/td>td>用户ID(自增)/td>/tr>tr>td>username/td>td>char(50)/td>td>用户名/td>/tr>tr>td>password/td>td>char(50)/td>td>密码/td>/tr>tr>td>.../td>td>.../td>td>.../td>/tr>tr>td>span stylecolor: #3366ff;>status/span>/td>td>span stylecolor: #3366ff;>int(11)/span>/td>td>span stylecolor: #3366ff;>用户状态位/span>/td>/tr>/tbody>/table>p>然后我们给每一种状态设定一个对应的值,这个值需要使用2的N次方来表示,比如:/p>table>tbody>tr>td>1/td>td>已确认资料/td>/tr>tr>td>2/td>td>已确认协议/td>/tr>tr>td>4/td>td>已冻结/td>/tr>tr>td>8/td>td>付费用户/td>/tr>tr>td>16/td>td>优质用户/td>/tr>/tbody>/table>p>span stylefont-size: 14pt;>strong>查询技巧示例:/strong>/span>/p>p>1、查询所有已冻结的用户:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-sql>SELECT * FROM `user` WHERE `status` & 4 4/code>/pre>p>/p>p>2、查询所有未确认协议的用户/p>pre classline-numbers prism-highlight data-start1>code classlanguage-sql>SELECT * FROM `user` WHERE `status` & 2 0/code>/pre>p>/p>p>3、设置ID等于186的用户为优质用户/p>pre classline-numbers prism-highlight data-start1>code classlanguage-sql>UPDATE `user` SET `status` `status` | 16 WHERE `id` 186/code>/pre>p>/p>p>4、取消ID等于186的优质用户身份/p>pre classline-numbers prism-highlight data-start1>code classlanguage-sql>UPDATE `user` SET `status` `status` ^ 16 WHERE `id` 186/code>/pre>p>/p>p>5、查询已冻结的优质付费用户(4 + 8 + 16 28)/p>pre classline-numbers prism-highlight data-start1>code classlanguage-sql>SELECT * FROM `user` WHERE `status` & 28 28/code>/pre>p>/p>p>同理,上述应用场景二中的分组问题,也可以使用同样的方法,优化表结构!然后使用位运算,实现各种条件的查询!/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/168.html title秒懂系列|Apache用户认证配置之Basic认证>秒懂系列|Apache用户认证配置之Basic认证/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2017-01-06T15:47:04+08:00>2017.01.06/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/server relcategory tag>Apache/Nginx/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/apache reltag>Apache/a>a hrefhttps://979137.com/archives/tag/web_server reltag>web服务器/a>a hrefhttps://979137.com/archives/tag/security reltag>安全/a>/li> /ul> /div> div classpost_content clearfix> p>很多时候我们可能需要对服务器资源进行保护,通常的做法是在应用层通过鉴权来实现,如果你嫌自己去实现鉴权太麻烦,那就直接让Apache去帮你实现吧!br />Apache常见的用户认证可以分为下面三种:br />- 基于IP,子网的访问控制(ACL)br />- 基本用户验证(Basic Authentication)br />- 消息摘要式身份验证(Digest Authentication)/p>p>基于IP的访问控制可以通过配置 Allow From实现!这里不多讲。br />一般的,我们还会在IP的基础上,再增加一层 Basic Authentication,实现一个基本的服务器用户认证!/p>h4>1、生成用户名密码文件/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>/usr/local/apache2/bin/htpasswd -bc users.pwd test hehe1234/code>/pre>blockquote>p> Adding password for user test/p>/blockquote>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>/usr/local/apache2/bin/htpasswd -b users.pwd test2 hehe4321/code>/pre>blockquote>p> Adding password for user test2/p>/blockquote>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>cat users.pwd/code>/pre>blockquote>p> test:$apr1$4R3foyQ5$1KGHVA5HQL8M9b0K/2UWO0br /> test2:$apr1$pKLy86CD$W9hFUvs4F06OBXtQhCbPV//p>/blockquote>p>可以看到用户名密码文件已经生成了,一行一个!/p>h4>2、配置 VirtualHost,如:/h4>pre classline-numbers prism-highlight data-start1>code classlanguage-shell><VirtualHost *:80> DocumentRoot /usr/local/www/pma/ DirectoryIndex index.php index.html index.shtml ServerName pma.979137.com CustomLog logs/pma.979137.com-access_log common ErrorLog logs/pma.979137.com-error_log <Directory /usr/local/www/pma/> Options Includes FollowSymLinks AllowOverride AuthConfig AuthName PMA Contents. AuthType basic AuthUserFile /usr/local/apache/conf/users.pwd Require valid-user </Directory></VirtualHost>/code>/pre>ul>li>AllowOverride 表示通过配置文件进行身份验证/li>li>AuthName 发送给客户端报文头内容:WWW-Authenticate/li>li>AuthType 认证类型/li>/li>li>AuthUserFile 这个就是刚刚生成的用户名密码文件/li>li>Require 指定哪些用户或组才能被授权访问。如:ul>li>require user test test2(只有用户 test 和 test2 可以访问)/li>li>requires groups managers (只有组 managers 中成员可以访问)/li>li>require valid-user (在 AuthUserFile 指定的文件中任何用户都可以访问)/li>/ul>/li>/ul>h4>我们来看一下效果:/h4>p>在浏览器访问:br />img srchttps://979137.com/wp-content/uploads/201701/06/151001z21828x38q1e4zpx.jpg alt />/p>p>cURL请求:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-null>curl -v http://pma.979137.com/test.php/code>/pre>blockquote>p>* Trying 10.223.28.1...br />* Connected to pma.979137.com (10.223.28.1) port 80 (#0)br />> GET /test.php HTTP/1.1br />> Host: pma.979137.combr />> User-Agent: curl/7.43.0br />> Accept: */*>br /> HTTP/1.1 401 Authorization Required Date: Fri, 06 Jan 2017 07:02:15 GMT Server: Apache/2.2.27 (Unix) PHP/5.3.29 WWW-Authenticate: Basic realm PMA Contents. Content-Length: 490 Content-Type: text/html; charsetiso-8859-1>401 Authorization Requiredbr />>Authorization Requiredbr />This server could not verify that youbr />are authorized to access the documentbr />requested. Either you supplied the wrongbr />credentials (e.g., bad password), or yourbr />browser doesnt understand how to supplybr />the credentials required./p>/blockquote>p>在没有携带用户名和密码时,HTTP Code 返回了 401,并输出了 Authorization Required!br />表示需要该请求需要进行认证!/p>p>我们再来看下,携带密码请求:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>curl -v -u test:hehe1234 http://pma.979137.com/test.php/code>/pre>blockquote>p>* Trying 10.223.28.1...br />* Connected to pma.979137.com (10.223.28.1) port 80 (#0)br />* Server auth using Basic with user testbr />> GET /test.php HTTP/1.1br />> Host: pma.979137.combr />> Authorization: Basic c2hpbGlhbmd4aWU6YWl5aTEzMTQbr />> User-Agent: curl/7.43.0br />> Accept: */*br />>br /> HTTP/1.1 200 OK Date: Fri, 06 Jan 2017 07:14:14 GMT Server: Apache/2.2.27 (Unix) PHP/5.3.29 X-Powered-By: PHP/5.3.29 Content-Length: 25 Content-Type: text/htmlstring(11) hello word!/p>/blockquote>p>HTTP Code 已经是 200 了,并且返回了正确的内容!br />至此,一个简单的 Basic 认证就OK了!这种认证一般可用于浏览器访问,也可以用于 API 认证!/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/167.html title秒懂系列|Apache和NGINX如何按天分割日志>秒懂系列|Apache和NGINX如何按天分割日志/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2016-12-16T19:35:02+08:00>2016.12.16/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/server relcategory tag>Apache/Nginx/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/apache reltag>Apache/a>a hrefhttps://979137.com/archives/tag/logrotate reltag>Logrotate/a>a hrefhttps://979137.com/archives/tag/nginx reltag>Nginx/a>a hrefhttps://979137.com/archives/tag/web_server reltag>web服务器/a>/li> /ul> /div> div classpost_content clearfix> p>默认情况下code>Apache/code>和code>NGINX/code>等web服务器,是没有帮我们按天分日志的,而是把所有日志放到一个文件,当流量大而日志多的时候,管理起来非常方便,一方面是内容多不易查找定位问题,另一方面文件也会越来越大,无效内容越来越多。br />常规做法是把日志按天分割,网上很多通过 写脚本+定时任务 来做的,这其实有点多余,而且不好维护,Linux本身自带了很多日志处理程序,比如:logrotate ,它可以实现日志的自动滚动,日志归档等功能。br />下面我们介绍使用 logrotate 实现br />- Apache按天分割日志br />- Nginx按天分割日志/p>h4>Apache/h4>p>Apache自身即成了 logrotate,一般在 /usr/local/apache2/bin/rotatelogsbr />先找到你的 rotatelogs 路径/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>locate rotatelogs/code>/pre>blockquote>p> /usr/local/apache2/bin/rotatelogs/p>/blockquote>p>在你的 VirtualHost 模块添加或替换/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell><VirtualHost *:80> ...... ErrorLog | /usr/local/apache2/bin/rotatelogs /data/logs/apache/979137.com-error_log-%Y%m%d 86400 480 CustomLog | /usr/local/apache2/bin/rotatelogs /data/logs/apache/979137.com-access_log-%Y%m%d 86400 480 common</VirtualHost>/code>/pre>p>指定分割时间:86400 默认单位为s。也就是24小时br />指定分区时差:480 默认单位m,也就是8小时/p>p>只保留30天日志,自动清理脚本:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-null>#!/bin/bashLOG_PATH_APACHE/data/logs/apache/DAYS_AGOdate -d -30 day +%Y%m%d\rm -rf ${LOG_PATH_APACHE}*log-${DAYS_AGO}/code>/pre>p>加入到crontab,每天执行一次即可/p>pre classline-numbers prism-highlight data-start1>code classlanguage-null>0 4 * * * /bin/bash /data/cron/clear_logs.sh > /dev/null 2&>1/code>/pre>h4>NGINX/h4>p>假设:br />- 日志在 /data/logs/nginx/ 下面br />- Nginx 安装在 /usr/local/nginx//p>p>1、在 /etc/logrotate.d 目录下创建一个 Nginx 的配置文件 Nginx 配置内容如下/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>/data/logs/nginx/xxx.qq.com-access_log /data/logs/nginx/xxx.qq.com-error_log { su nobody nobody daily rotate 30 notifempty sharedscripts postrotate if -f /usr/local/nginx/logs/nginx.pid ; then /bin/kill -USR1 /bin/cat /usr/local/nginx/logs/nginx.pid fi endscript}/code>/pre>p>配置解释:br />1)配置需要分割的日志文件,也可以用 * 代替br />2)su nobody nobody :一般我们的日志目录是允许所有用户进行写操作的,logrotate 认为这不是不安全的,这时 logrotate 会报错如下:/p>blockquote>p>error: skipping /data/logs/nginx/cafe.qq.com-error_log because parent directory has insecure permissions (Its world writable or writable by group which is not root) Set su directive in config file to tell logrotate which user/group should be used for rotation./p>/blockquote>p>所以我们需要在配置里使用 su 切换身份执行br />3)daily :日志文件每天进行滚动br />4)rotate 30 :保留30天的日志br />5)notifempty:日志文件为空不进行滚动br />6)sharedscripts :运行postrotate脚本br />9)/bin/kill -USR1 \/bin/cat /usr/local/nginx/logs/nginx.pid\ :Nginx平滑重启,也即让Nginx重新生成文件/p>p>2、执行logrotate/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>/usr/sbin/logrotate -f /etc/logrotate.d/nginx/code>/pre>p>如此,Nginx就会自动的按天产生日志了!/p> /div> /article> article classarticle clearfix> h2 classtitle entry-title>a hrefhttps://979137.com/archives/166.html title简单实用的PHPMonitor:运行错误监控>简单实用的PHPMonitor:运行错误监控/a>/h2> div classpost_meta top clearfix> ul classclearfix> li classpost_date>time classentry-date updated datetime2016-12-07T00:03:03+08:00>2016.12.07/time>/li> li classpost_author vcard author>span classfn>a hrefhttps://979137.com/archives/author/979137 title文章作者 云厉 relauthor>云厉/a>/span>/li> li classpost_category>a hrefhttps://979137.com/archives/category/php relcategory tag>PHP/a>/li> li classpost_tag>a hrefhttps://979137.com/archives/tag/elasticsearch reltag>ElasticSearch/a>a hrefhttps://979137.com/archives/tag/elk reltag>ELK/a>a hrefhttps://979137.com/archives/tag/kibana reltag>Kibana/a>a hrefhttps://979137.com/archives/tag/logstash reltag>Logstash/a>a hrefhttps://979137.com/archives/tag/php reltag>PHP/a>a hrefhttps://979137.com/archives/tag/restful reltag>RESTful/a>a hrefhttps://979137.com/archives/tag/monitor reltag>监控/a>a hrefhttps://979137.com/archives/tag/error_handling reltag>错误处理/a>/li> /ul> /div> div classpost_content clearfix> blockquote>p>所有运行在线网环境的程序应该都被监控,对线网而言,无论是 Fatal error、E_NOTICE 还是 E_STRICT,都应该被消灭。对开发者而言,线网发生任何异常或潜在bug时,应该第一时间修复、优化,而不是等反馈之后才知道,然后临时去排查问题!/p>/blockquote>h3>这篇文章要讲啥?/h3>ol start1.>li>了解PHP错误机制/li>li>注册PHP的异常处理函数、错误处理函数、脚本退出函数/li>li>配置PHP预加载(重要)/li>li>搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)/li>li>基于 ElasticSearch RESTful 实现告警/li>/ol start1.>p>先看一张系统全局图br />img srchttps://979137.com/wp-content/uploads/2016/12/1494388404_12_w662_h518.png alt />/p>h3>要达到的目的?/h3>p>所有PHP程序里的错误或潜在错误(支持自定义)都全部写入日志并告警,包含错误的所在文件名、行号、函数(如果有)、错误信息等等br />PHP程序不需要做任何修改或接入!/p>h3>一、了解PHP错误机制/h3>p>截止PHP7.1,PHP共有16个错误级别:br />a hrefhttps://php.net/manual/zh/errorfunc.constants.php titlehttps://php.net/manual/zh/errorfunc.constants.php>https://php.net/manual/zh/errorfunc.constants.php/a>/p>p>如果你对PHP错误机制还不太了解,网上有很多关于a hrefhttps://www.google.com.hk/#newwindow1&safestrict&qPHP%E9%94%99%E8%AF%AF%E6%9C%BA%E5%88%B6%E6%80%BB%E7%BB%93 titlePHP错误机制的总结>PHP错误机制的总结/a>,因为这个不是文章的重点,这里不再详细介绍!/p>h3>二、注册PHP的异常处理函数、错误处理函数、脚本退出函数/h3>p>PHP有三个很重要的注册回调函数的函数br />- register_shutdown_function 注册PHP退出时的回调函数br />- set_error_handler 注册错误处理函数br />- set_exception_handler 注册异常处理函数/p>p>我们先定义一个日志处理类,专门用于写日志(也可以用于用户日志哦)/p>pre classline-numbers prism-highlight data-start1>code classlanguage-php><?php/** * 日志接口 * @filename Loger.php * @since 2016-12-08 12:13:50 * @author 979137.com * @version $Id$ */class Loger { // 日志目录,建议独立于Web目录,这个目录将作用于 Logstash 日志收集 protected static $log_path /data/logs/; // 日志类型 protected static $type array(ERROR, INFO, WARN, CRITICAL); /** * 写入日志信息 * @param mixed $_msg 调试信息 * @param string $_type 信息类型 * @param string $file_prefix 日志文件名默认取当前日期,可以通过文件名前缀区分不同的业务 * @param array $trace TRACE信息,如果为空,则会从debug_backtrace里获取 * @return bool */ public static function write($_msg, $_type info, $file_prefix , $trace array()) { $_type strtoupper($_type); $_msg is_string($_msg) ? $_msg : var_export($_msg, true); if (!in_array($_type, self::$type)) { return false; } $server isset($_SERVERSERVER_ADDR) ? $_SERVERSERVER_ADDR : 0.0.0.0; $remote isset($_SERVERREMOTE_ADDR) ? $_SERVERREMOTE_ADDR : 0.0.0.0; $method isset($_SERVERREQUEST_METHOD) ? $_SERVERREQUEST_METHOD : CLI; if (!is_array($trace) || empty($trace)) { $dtrace debug_backtrace(); $trace $dtrace0; if (count($dtrace) 1) { //不是在类或函数内调用 $tracefunction ; } else { if ($dtrace1function __callStatic) { $tracefile $dtrace2file; $traceline $dtrace2line; $tracefunction empty($dtrace3function) ? : $dtrace3function; } else { $tracefunction $dtrace1function; } } } $ace $trace; $now date(Y-m-d H:i:s); $pre {$now}%s{$acefile}{$aceline}{$acefunction}{$remote}{$method}{$server}%s; $msg sprintf($pre, $_type, $_msg); $filename phplog_ . ($file_prefix ?: netbar) . _ . date(Ymd) . .log; $destination self::$log_path . $filename; is_dir(self::$log_path) || mkdir(self::$log_path, 0777, true); //文件不存在,则创建文件并加入可写权限 if (!file_exists($destination)) { touch($destination); chmod($destination, 0777); } return error_log($msg.PHP_EOL, 3, $destination) ?: false; } /** * 静态魔术调用 * @param $method * @param $args * @return mixed * * @method void error($msg) static * @method void info($msg) static * @method void warn($msg) static */ public static function __callStatic($method, $args) { $method strtoupper($method); if (in_array($method, self::$type)) { $_msg array_shift($args); return self::write($_msg, $method); } return false; }}/code>/pre>p>/p>p>接下来,再写一个系统处理类,定义回调函数和注册回调函数/p>pre classline-numbers prism-highlight data-start1>code classlanguage-php><?php/** * 注册系统处理函数 * @filename Handler.php * @since 2016-12-08 12:13:50 * @author 979137.com * @version $Id$ */class Handler { const LOG_FILE_PREFIX handler; const LOG_TYPE CRITICAL; /** * 函数注册 * @return none */ static public function set() { //注册致命错误处理方法 register_shutdown_function(array(__CLASS__, fatalError)); //注册自定义错误处理方法 set_error_handler(array(__CLASS__, appError)); //注册异常处理方法 set_exception_handler(array(__CLASS__, appException)); } /** * 致命错误捕获,PHP错误级别预定义常量参考: * http://php.net/manual/zh/errorfunc.constants.php * @return none */ static public function fatalError() { $error error_get_last() ?: null; if (!is_null($error) && in_array($errortype, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) { $errorclass $errorfunction ; Loger::write($errormessage, self::LOG_TYPE, self::LOG_FILE_PREFIX, $error); self::halt($error); } } /** * 自定义错误处理 * @param int $errno 错误类型 * @param string $errstr 错误信息 * @param string $errfile 错误文件 * @param int $errline 错误行数 * @return void */ static public function appError($errno, $errstr, $errfile, $errline) { $errormessage $errno $errstr; $errorfile $errfile; $errorline $errline; $errorclass $errorfunction ; if (!in_array($errno, array(E_STRICT, E_DEPRECATED))) { Loger::write($errormessage, self::LOG_TYPE, self::LOG_FILE_PREFIX, $error); if (in_array($errno, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) { self::halt($error); } } } /** * 自定义异常处理 * @param mixed $e 异常对象 * @return void */ static public function appException($e) { $error array(); $errormessage $e->getMessage(); $errorfile $e->getFile(); $errorline $e->getLine(); $trace $e->getTrace(); if(empty($trace0function) && $trace0function exception) { $errorfile $trace0file; $errorline $trace0line; } //$errortrace $e->getTraceAsString(); $errorfunction $errorclass ; Loger::write($errormessage, self::LOG_TYPE, self::LOG_FILE_PREFIX, $error); self::halt($error); } /** * 错误输出 * @param mixed $error 错误 * @return void */ static public function halt($error) { ob_get_contents() && ob_end_clean(); $e array(); if (IS_DEBUG || IS_CLI) { //调试模式下输出错误信息 $e $error; if(IS_CLI){ $e_message $emessage. in .$efile. on line .$eline.PHP_EOL; if (isset($etreace) ) { $e_message . $etrace; } exit($e_message); } } else { //线网不显示错误信息,显示固定字符串,保护系统安全 //TODO:比较友好的做法是重定向到一个漂亮的错误页面 exit(Sorry, the system error); } // 包含异常页面模板 $exceptionFile __DIR__ . /exception.tpl; include $exceptionFile; exit(0); }}/code>/pre>p>/p>h3>二、自动加载脚本(auto_append_file)/h3>p>以上两个脚本准备就绪以后,我们可以把它们合并到一个文件,并增加两个重要常量:/p>pre classline-numbers prism-highlight data-start1>code classlanguage-php><?php//是否CLI模式define(IS_CLI, PHP_SAPI cli ? true : false);//当前是否开发模式,用于区分线网和开发模式,默认false//开发模式下,所有错误会打印出来。非开发模式下,不会打印到页面,但会记录日志define(IS_DEBUG, isset($_SERVERDEV_ENV) ? true : false);//注册系统处理函数Handler::set();/code>/pre>p>假设合并后的文件名叫:code>auto_prepend_file.php/code>br />我们把这个文件进行预加载(即自动包含进所有PHP脚本)br />这时候到了很重要的一步就是,就是配置 code>php.ini/code>/p>blockquote>p> auto_prepend_file /your_path/auto_prepend_file.php/p>/blockquote>p>重启你的Web服务器如 code>Apache/code>、code>Nginx/code>br />写一个测试脚本 test.php/p>pre classline-numbers prism-highlight data-start1>code classlanguage-php><?phpvar_dump($tencent);/code>/pre>p>因为 code>$tencent/code> 未定义,所以这时候就会回调我们注册的函数,可以看到已经有错误日志了/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>php -f test.phptail -f phplog_handler_20161209.log /code>/pre>blockquote>p> 2016-12-09 16:01:05CRITICAL/data/logs/test.php20.0.0.0CLI0.0.0.08 Undefined variable: tencent/p>/blockquote>p>/p>h3>三、搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)/h3>p>因为我们的Web服务器一般都是分布式的,线网可能有N台服务器,我们的日志又是写本地,所以这时候就需要一个日志中心了,br />日志系统目前已经有很多成熟的解决方案了,这里推荐 code>ELK/code> 部署,br />code>ElasticSearch/code>,一个基于 code>Lucene/code> 的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于 code>RESTful/code> 架构。br />code>Logstash/code>,分布式日志收集、分析、存储工具br />code>Kibana/code>,基于 code>ElasticSearch/code> 的前端展示工具br />因为网上已经很多 code>ELK/code> 的搭建教程了,这里就不重复写了!/p>p>下面是一个基于ELK搭建好的日志中心,好不好用,用了就知道了!^_^br />img srchttps://979137.com/wp-content/uploads/2016/12/1481271645_19_w1182_h785.jpg alt />br />/p>h3>四、利用 ElasticSearch RESTful 实现告警功能/h3>p>前面我们提到 ElasticSearch 是基于 RESTful 架构!br />ElasticSearch 提供了非常多的接口,包括strong>索引/strong>的增删改查,strong>文档/strong>的增删改查,各种strong>搜索/strong>/p>p>code>ElasticSearch/code>官方提供了一个code>PHP/code>版本的code>SDK/code>:code>Elasticsearch-PHP/code>br />官方文档(全英文,目前没有中文版,看起来可能吃力,英文就英文吧,认真啃):br />https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html/p>p>我们要做告警要用到的就是em>a hrefhttps://www.elastic.co/guide/en/elasticsearch/client/php-api/1.0/_search_operations.html title搜索接口>搜索接口/a>/em>!br />说白了,就是定时去把前面写的那些日志,全扫出来,然后告警给关系人!/p>p>code>Elasticsearch-PHP/code> 安装很简单,通过 code>Composer/code> 来安装,我这里以 code>1.0/code> 为例子br />strong>1、编写一个 composer.json 文件/strong>/p>pre classline-numbers prism-highlight data-start1>code classlanguage-json>{ require: { elasticsearch/elasticsearch: ~1.0 }}/code>/pre>p>/p>p>strong>2、cd到你的项目里,下载安装包/strong>/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>curl -s http://getcomposer.org/installer | php/code>/pre>p>/p>p>strong>3、安装 code>Elasticsearch-PHP/code> 及其依赖/strong>/p>pre classline-numbers prism-highlight data-start1>code classlanguage-shell>php composer.phar install --no-dev/code>/pre>p>/p>p>完事之后,就可以写告警脚本了!下面是我写的一个示例/p>pre classline-numbers prism-highlight data-start1>code classlanguage-php><?php//ElasticSearch Server,这是你的ELK服务器和端口$es_server array( 127.0.0.1:9200,);//接受告警人员$alert_staff array( shiliangxie > 18666665940, xieshiliang > 18600005940,);//需要告警的错误级别$alert_level array(ERROR, WARN, CRITICAL);//告警周期,单位:分钟,这个需要和 cron 设置成一样的时间$alert_cycle 10;###################################################################//搜索最近 $alert_cycle 的错误日志$qindex logstash-.date(Y.m.d);$qignore_unavailable true;$qtype php;$qsize 500;$qsort array(@timestamp:desc);$range_timelte sprintf(%.3f, microtime(true)) * 1000;$range_timegte $range_timelte - $alert_cycle * 60 * 1000;$range array(@timestamp > $range_time);$match array(level > implode( OR , $alert_level));$qbodyqueryfiltered array( filter > array(range > $range), query > array(match > $match),);$paramshosts $es_server;require __DIR__./vendor/autoload.php;$client new Elasticsearch\Client($params);$ret $client->search($q);$hit $rethitshits;$hit is_array($hit) && count($hit) ? $hit : array();//组织告警内容$alerts array();foreach($hit as $h) { $source $h_source; $tag md5($sourcelevel . $sourcefile . $sourceline. $sourcemsg); if (isset($alerts$tag)) { $alerts$tagnum++; } else { $alerts$tagnum 1; $content %s%s%s%s%s; $alerts$tagcontent sprintf($content, date(Y-m-d H:i:s, strtotime($source@timestamp)), $sourcelevel, $sourcefile, $sourceline, $sourcemsg ); }}//var_dump($alerts);exit;//发送告警if (is_array($alerts) && count($alerts)) { array_walk($alerts, function($alert, $tag) use($alert_staff) { $msg sprintf(Repeat:%d%s, $alertnum, $alertcontent); array_map(function($mobile) use($msg) { return send_sms($mobile, $msg); }, $alert_staff); return ; });}/code>/pre>p>然后我们把告警脚本加到 code>crontab/code> 即可/p>blockquote>p> */10 * * * * /usr/local/bin/php /data/cron/PHPMonitor.php/p>/blockquote>p>坐等告警吧!br />img srchttps://979137.com/wp-content/uploads/2016/12/1481279346_52_w400_h712.jpg alt />/p>p>对于报警方案,目前市场上也有很多实现方案,如 code>Yelp/code> 公司的 code>ElastAlert/code>,用 code>Python/code> 写的一个基于 code>ELK/code> 用 code>Elasticsearch/code> code>RESTful/code> 报警框架/p> /div> /article> /section>!-- END #post_list --> div classpage_navi clearfix>h4>PAGE NAVI/h4>ul classpage-numbers> li>span aria-currentpage classpage-numbers current>1/span>/li> li>a classpage-numbers hrefhttps://979137.com/page/2>2/a>/li> li>a classpage-numbers hrefhttps://979137.com/page/3>3/a>/li> li>a classpage-numbers hrefhttps://979137.com/page/4>4/a>/li> li>a classpage-numbers hrefhttps://979137.com/page/5>5/a>/li> li>a classpage-numbers hrefhttps://979137.com/page/6>6/a>/li> li>span classpage-numbers dots>…/span>/li> li>a classpage-numbers hrefhttps://979137.com/page/8>8/a>/li> li>a classnext page-numbers hrefhttps://979137.com/page/2>»/a>/li>/ul>/div>/div>!-- END #mail_col -->div idside_col classclearfix> div idside_content> div classside_widget clearfix widget_media_image idmedia_image-3>h3 classside_headline>微信公众号:程序员到架构师/h3>img width260 height260 srchttps://979137.com/wp-content/uploads/2018/05/qrcode_for_gh_aba1b066e2ea_430.jpg classimage wp-image-1079 attachment-full size-full alt loadinglazy stylemax-width: 100%; height: auto; srcsethttps://979137.com/wp-content/uploads/2018/05/qrcode_for_gh_aba1b066e2ea_430.jpg 260w, https://979137.com/wp-content/uploads/2018/05/qrcode_for_gh_aba1b066e2ea_430-150x150.jpg 150w sizes(max-width: 260px) 100vw, 260px />/div> div classside_widget clearfix widget_recent_entries idrecent-posts-4> h3 classside_headline>最新文章/h3> ul> li> a hrefhttps://979137.com/archives/1135.html>ZooKeeper是如何实现数据一致性的?/a> /li> li> a hrefhttps://979137.com/archives/1084.html>Python实战之520特别版:用微信每天和她说晚安/a> /li> li> a hrefhttps://979137.com/archives/1066.html>Python中被忽略的else/a> /li> li> a hrefhttps://979137.com/archives/1039.html>准确判断文件类型方法之二进制文件头/a> /li> li> a hrefhttps://979137.com/archives/870.html>巧用Terraform完成腾讯云上自动运维/a> /li> li> a hrefhttps://979137.com/archives/518.html>腾讯云支持Terraform开发实践/a> /li> li> a hrefhttps://979137.com/archives/169.html>MySQL位运算的应用/a> /li> li> a hrefhttps://979137.com/archives/168.html>秒懂系列|Apache用户认证配置之Basic认证/a> /li> li> a hrefhttps://979137.com/archives/167.html>秒懂系列|Apache和NGINX如何按天分割日志/a> /li> li> a hrefhttps://979137.com/archives/166.html>简单实用的PHPMonitor:运行错误监控/a> /li> li> a hrefhttps://979137.com/archives/164.html>PHP实现MySQL并发查询/a> /li> li> a hrefhttps://979137.com/archives/156.html>UNIX/Linux命令行提示符显示当前完整路径的方法/a> /li> li> a hrefhttps://979137.com/archives/155.html>秒懂系列|Apache和NGINX退出自动重启/a> /li> li> a hrefhttps://979137.com/archives/154.html>-bash: ./configure: /bin/sh^M: bad interpreter: No such file or directory/a> /li> li> a hrefhttps://979137.com/archives/153.html>用10条命令在1分钟内检查Linux服务器性能/a> /li> li> a hrefhttps://979137.com/archives/151.html>HTML也可以实现文件包含/a> /li> li> a hrefhttps://979137.com/archives/148.html>Linux实现IP和端口转发方法/a> /li> li> a hrefhttps://979137.com/archives/145.html>Discuz在新浪云空间实现URL伪静态/a> /li> li> a hrefhttps://979137.com/archives/138.html>php –with-mysqlimysqlnd –with-pdo-mysqlmysqlnd/a> /li> li> a hrefhttps://979137.com/archives/135.html>Starting MySQL… ERROR! The server quit without updating PID file/a> /li> li> a hrefhttps://979137.com/archives/133.html>PHP也支持多线程:cURL并发请求/a> /li> li> a hrefhttps://979137.com/archives/132.html>Redis、Memcache、MongoDB的区别与关系和作用/a> /li> li> a hrefhttps://979137.com/archives/131.html>在新浪云SAE架构一个大型Web应用到底有多简单/a> /li> li> a hrefhttps://979137.com/archives/130.html>10分钟完成微信公众号第三方平台全网发布/a> /li> li> a hrefhttps://979137.com/archives/127.html>国内外免费CDN公共JS库,前端公共库、常用JavaScript库CDN服务/a> /li> li> a hrefhttps://979137.com/archives/125.html>不怕路长,只怕心老/a> /li> li> a hrefhttps://979137.com/archives/124.html>回归你的简单生活/a> /li> li> a hrefhttps://979137.com/archives/123.html>JS实现完美身份证号有效性验证/a> /li> li> a hrefhttps://979137.com/archives/122.html>隐藏字符串中部分字符的PHP函数,如:姓名、用户名、身份证、IP、手机号等/a> /li> li> a hrefhttps://979137.com/archives/119.html>如何保存一个网页至桌面上成为快捷方式,PHP生成桌面快捷方式/a> /li> /ul> /div>div classside_widget clearfix widget_tag_cloud idtag_cloud-3>h3 classside_headline>标签云/h3>div classtagcloud>a hrefhttps://979137.com/archives/tag/apache classtag-cloud-link tag-link-38 tag-link-position-1 stylefont-size: 17.77358490566pt; aria-labelApache (10个项目)>Apache/a>a hrefhttps://979137.com/archives/tag/api classtag-cloud-link tag-link-30 tag-link-position-2 stylefont-size: 10.377358490566pt; aria-labelAPI (2个项目)>API/a>a hrefhttps://979137.com/archives/tag/curl classtag-cloud-link tag-link-58 tag-link-position-3 stylefont-size: 11.962264150943pt; aria-labelcURL (3个项目)>cURL/a>a hrefhttps://979137.com/archives/tag/discuz classtag-cloud-link tag-link-46 tag-link-position-4 stylefont-size: 10.377358490566pt; aria-labelDiscuz (2个项目)>Discuz/a>a hrefhttps://979137.com/archives/tag/golang classtag-cloud-link tag-link-76 tag-link-position-5 stylefont-size: 8pt; aria-labelGolang (1个项目)>Golang/a>a hrefhttps://979137.com/archives/tag/html classtag-cloud-link tag-link-20 tag-link-position-6 stylefont-size: 8pt; aria-labelHTML (1个项目)>HTML/a>a hrefhttps://979137.com/archives/tag/http classtag-cloud-link tag-link-70 tag-link-position-7 stylefont-size: 8pt; aria-labelHTTP (1个项目)>HTTP/a>a hrefhttps://979137.com/archives/tag/https classtag-cloud-link tag-link-71 tag-link-position-8 stylefont-size: 8pt; aria-labelHTTPS (1个项目)>HTTPS/a>a hrefhttps://979137.com/archives/tag/js classtag-cloud-link tag-link-22 tag-link-position-9 stylefont-size: 10.377358490566pt; aria-labelJavaScript (2个项目)>JavaScript/a>a hrefhttps://979137.com/archives/tag/linux classtag-cloud-link tag-link-12 tag-link-position-10 stylefont-size: 17.245283018868pt; aria-labelLinux (9个项目)>Linux/a>a hrefhttps://979137.com/archives/tag/logrotate classtag-cloud-link tag-link-48 tag-link-position-11 stylefont-size: 8pt; aria-labelLogrotate (1个项目)>Logrotate/a>a hrefhttps://979137.com/archives/tag/memcache classtag-cloud-link tag-link-15 tag-link-position-12 stylefont-size: 8pt; aria-labelMemcache (1个项目)>Memcache/a>a hrefhttps://979137.com/archives/tag/mongodb classtag-cloud-link tag-link-16 tag-link-position-13 stylefont-size: 8pt; aria-labelMongoDB (1个项目)>MongoDB/a>a hrefhttps://979137.com/archives/tag/mysql classtag-cloud-link tag-link-14 tag-link-position-14 stylefont-size: 17.245283018868pt; aria-labelMySQL (9个项目)>MySQL/a>a hrefhttps://979137.com/archives/tag/nginx classtag-cloud-link tag-link-47 tag-link-position-15 stylefont-size: 13.283018867925pt; aria-labelNginx (4个项目)>Nginx/a>a hrefhttps://979137.com/archives/tag/oauth classtag-cloud-link tag-link-84 tag-link-position-16 stylefont-size: 8pt; aria-labelOAuth (1个项目)>OAuth/a>a hrefhttps://979137.com/archives/tag/observer classtag-cloud-link tag-link-90 tag-link-position-17 stylefont-size: 8pt; aria-labelobserver (1个项目)>observer/a>a hrefhttps://979137.com/archives/tag/php classtag-cloud-link tag-link-17 tag-link-position-18 stylefont-size: 22pt; aria-labelPHP (22个项目)>PHP/a>a hrefhttps://979137.com/archives/tag/provider classtag-cloud-link tag-link-75 tag-link-position-19 stylefont-size: 8pt; aria-labelProvider (1个项目)>Provider/a>a hrefhttps://979137.com/archives/tag/python classtag-cloud-link tag-link-18 tag-link-position-20 stylefont-size: 11.962264150943pt; aria-labelPython (3个项目)>Python/a>a hrefhttps://979137.com/archives/tag/restful classtag-cloud-link tag-link-55 tag-link-position-21 stylefont-size: 11.962264150943pt; aria-labelRESTful (3个项目)>RESTful/a>a hrefhttps://979137.com/archives/tag/sae classtag-cloud-link tag-link-45 tag-link-position-22 stylefont-size: 15.264150943396pt; aria-labelSAE (6个项目)>SAE/a>a hrefhttps://979137.com/archives/tag/shell classtag-cloud-link tag-link-27 tag-link-position-23 stylefont-size: 10.377358490566pt; aria-labelShell (2个项目)>Shell/a>a hrefhttps://979137.com/archives/tag/terraform classtag-cloud-link tag-link-74 tag-link-position-24 stylefont-size: 10.377358490566pt; aria-labelTerraform (2个项目)>Terraform/a>a hrefhttps://979137.com/archives/tag/web_server classtag-cloud-link tag-link-49 tag-link-position-25 stylefont-size: 10.377358490566pt; aria-labelweb服务器 (2个项目)>web服务器/a>a hrefhttps://979137.com/archives/tag/zab classtag-cloud-link tag-link-89 tag-link-position-26 stylefont-size: 8pt; aria-labelZAB (1个项目)>ZAB/a>a hrefhttps://979137.com/archives/tag/zookeeper classtag-cloud-link tag-link-88 tag-link-position-27 stylefont-size: 8pt; aria-labelZooKeeper (1个项目)>ZooKeeper/a>a hrefhttps://979137.com/archives/tag/cloud classtag-cloud-link tag-link-65 tag-link-position-28 stylefont-size: 13.283018867925pt; aria-label云计算 (4个项目)>云计算/a>a hrefhttps://979137.com/archives/tag/code classtag-cloud-link tag-link-72 tag-link-position-29 stylefont-size: 8pt; aria-label代码 (1个项目)>代码/a>a hrefhttps://979137.com/archives/tag/kernel classtag-cloud-link tag-link-87 tag-link-position-30 stylefont-size: 8pt; aria-label内核 (1个项目)>内核/a>a hrefhttps://979137.com/archives/tag/function classtag-cloud-link tag-link-82 tag-link-position-31 stylefont-size: 14.339622641509pt; aria-label函数 (5个项目)>函数/a>a hrefhttps://979137.com/archives/tag/infrastructure classtag-cloud-link tag-link-78 tag-link-position-32 stylefont-size: 10.377358490566pt; aria-label基础架构 (2个项目)>基础架构/a>a hrefhttps://979137.com/archives/tag/security classtag-cloud-link tag-link-34 tag-link-position-33 stylefont-size: 11.962264150943pt; aria-label安全 (3个项目)>安全/a>a hrefhttps://979137.com/archives/tag/concurrent classtag-cloud-link tag-link-57 tag-link-position-34 stylefont-size: 11.962264150943pt; aria-label并发编程 (3个项目)>并发编程/a>a hrefhttps://979137.com/archives/tag/mp classtag-cloud-link tag-link-69 tag-link-position-35 stylefont-size: 8pt; aria-label微信公众号 (1个项目)>微信公众号/a>a hrefhttps://979137.com/archives/tag/qos classtag-cloud-link tag-link-67 tag-link-position-36 stylefont-size: 8pt; aria-label性能 (1个项目)>性能/a>a hrefhttps://979137.com/archives/tag/db classtag-cloud-link tag-link-33 tag-link-position-37 stylefont-size: 10.377358490566pt; aria-label数据库 (2个项目)>数据库/a>a hrefhttps://979137.com/archives/tag/sina_cloud classtag-cloud-link tag-link-44 tag-link-position-38 stylefont-size: 15.264150943396pt; aria-label新浪云 (6个项目)>新浪云/a>a hrefhttps://979137.com/archives/tag/crawler classtag-cloud-link tag-link-85 tag-link-position-39 stylefont-size: 10.377358490566pt; aria-label爬虫 (2个项目)>爬虫/a>a hrefhttps://979137.com/archives/tag/mobile classtag-cloud-link tag-link-86 tag-link-position-40 stylefont-size: 8pt; aria-label移动开发 (1个项目)>移动开发/a>a hrefhttps://979137.com/archives/tag/algorithm classtag-cloud-link tag-link-31 tag-link-position-41 stylefont-size: 10.377358490566pt; aria-label算法 (2个项目)>算法/a>a hrefhttps://979137.com/archives/tag/sa classtag-cloud-link tag-link-83 tag-link-position-42 stylefont-size: 11.962264150943pt; aria-label系统架构 (3个项目)>系统架构/a>a hrefhttps://979137.com/archives/tag/tencent_cloud classtag-cloud-link tag-link-77 tag-link-position-43 stylefont-size: 11.962264150943pt; aria-label腾讯云 (3个项目)>腾讯云/a>a hrefhttps://979137.com/archives/tag/op classtag-cloud-link tag-link-81 tag-link-position-44 stylefont-size: 8pt; aria-label运维 (1个项目)>运维/a>a hrefhttps://979137.com/archives/tag/error_handling classtag-cloud-link tag-link-51 tag-link-position-45 stylefont-size: 11.962264150943pt; aria-label错误处理 (3个项目)>错误处理/a>/div>/div> /div>!-- END #side_content -->/div>!-- END #side_col --> /div>!-- END #main_contents --> a idreturn_top href#header>Return Top/a> /div>!-- END #contents --> footer idfooter> div idfooter_nav_area> nav idfooter_menu classclearfix> ul idmenu-%e5%ba%95%e9%83%a8%e8%8f%9c%e5%8d%95 classmenu>li idmenu-item-387 classmenu-item menu-item-type-post_type menu-item-object-page menu-item-387>a hrefhttps://979137.com/about>关于云厉/a>/li>li idmenu-item-797 classmenu-item menu-item-type-post_type menu-item-object-page menu-item-797>a hrefhttps://979137.com/about>QQ微信: 979137/a>/li>li idmenu-item-313 classmenu-item menu-item-type-custom menu-item-object-custom menu-item-313>a target_blank relnoopener hrefmailto:979137@qq.com>979137@qq.com/a>/li>li idmenu-item-314 classmenu-item menu-item-type-custom menu-item-object-custom menu-item-314>a target_blank relnoopener hrefhttps://weibo.com/xieshiliang>微博/a>/li>li idmenu-item-315 classmenu-item menu-item-type-custom menu-item-object-custom menu-item-315>a target_blank relnoopener hrefhttps://github.com/979137>GitHub/a>/li>li idmenu-item-318 classmenu-item menu-item-type-custom menu-item-object-custom menu-item-318>a target_blank relnoopener hrefhttps://beian.miit.gov.cn/>粤ICP备18036866号-1/a>/li>li idmenu-item-792 classmenu-item menu-item-type-custom menu-item-object-custom menu-item-792>a target_blank relnoopener hrefhttp://www.beian.gov.cn/portal/registerSystemInfo?recordcode44030502001749>粤公网安备44030502001749号/a>/li>/ul> /nav> p idcopyright>span classcopyright>© 2010-2018 979137.com All Rights Reserved/span>/p> /div> /footer> /div>!-- END #container --> !-- mobile menu --> !-- This theme is created by mono-lab https://www.mono-lab.net --> script typetext/javascript>var cnzz_protocol ((https: document.location.protocol) ? https:// : http://);document.write(unescape(%3Cspan idcnzz_stat_icon_1273389659%3E%3C/span%3E%3Cscript src + cnzz_protocol + s13.cnzz.com/stat.php%3Fid%3D1273389659%26show%3Dpic typetext/javascript%3E%3C/script%3E));document.getElementById(cnzz_stat_icon_1273389659).style.display none;/script> /body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]