PHP- अनुप्रयोगों के श्रमसाध्य अनुकूलन (मैं PHP5 पर विचार करता हूं, लेकिन अधिकांश 4 वीं शाखा के लिए सही हैं)

जब एक सपने में वह सपने देखता है, "ओह, और अगर सर्वर पर्याप्त नहीं है ..."



शुरुआत के लिए, शुभ रात्रि। मैं कुछ उपयोगी लिख रहा हूं, जैसे पहली बार (मेरे ब्लॉग पर सभी प्रकार के अर्ध-परीक्षणों को छोड़कर)। मैं एक भयानक व्यक्ति हूँ, यह अचानक मेरे साथ हुआ कि मैं किसी को बहुत समय बचाने में मदद कर सकता हूँ;)।









सामान्य तौर पर, जब PHP (> 100,000 लाइनों के कोड) में काफी बड़ी परियोजनाएं बनाई जाती हैं, तो "सही" काम करने की इच्छा जो बहुत पहले की गई थी, वह सब कुछ अराजकता में डुबाने की धमकी देती है। कम से कम नए प्रोग्रामर्स के लिए जो सप्ताह, महीने, साल में कंपनी में आ सकते हैं ... समाधान शुरुआत से ही एक स्पष्ट व्यवस्थितकरण और सख्त वास्तु नियमों की स्थापना है। खुद के लिए, मैंने तय किया - फ्रेमवर्क का उपयोग किए बिना, मैं केवल "हैलो वर्ल्ड" साइटें लिखूंगा। आगे की हलचल के बिना, जब मैंने उन चौखटों के बारे में सोचा, जिन्हें मैंने पढ़ा, पढ़ा, लेकिन अपने ZendFramework के साथ खुद को समान रूप देने का फैसला किया। ताकतवर वह, हालांकि मैंने इसमें खुद के लिए भारी संख्या में बदलाव किए।





इस तरह के एक समाधान में, सभी संभव प्लसस और सुविधा के साथ, एक सवाल और एक दीवार अचानक उठती है: अब मेरा व्यवसाय तर्क पूरे कार्यक्रम के निष्पादन समय का लगभग 1-2% है। सुविधा और ओओपी के लिए शुल्क (या "ओओपी की सुविधा"; संभवतः यहां तक ​​कि "सुविधा" या सिर्फ "ओओपी" लगभग एक ही बात है;)) - संबंधित और नियंत्रण कोड की एक बड़ी राशि।





सामान्य तौर पर, जब मैं एक नई परियोजना कर रहा था - एक लक्ष्य था - बीजदार सेलेरोन 2.6GHz पर प्रति सेकंड कम से कम 50 अनुरोध। यानी लगभग 0.02 सेकंड प्रति अनुरोध, जिसमें mysql भी शामिल है और इसी तरह। परियोजना के निर्माण के दौरान, मैं कुछ सुधारों के साथ इसे कई बार ओवरक्लॉक करने में कामयाब रहा। कौन से हैं? एक कप कॉफी डालो - और बुद्धिमान विकास की दुनिया में आपका स्वागत है :) मुझे तुरंत कहना चाहिए - यह निकला।





MockSoul से सूप के लिए A से Z तक पकाने की विधि :)





स्टेज 0. तैयार होना





पर्यावरण? मेरी पसंदीदा योजना:





  1. Lighttpd। लिनक्स के तहत। के साथ sys-epoll चालू;



  2. PHP5। FastCGI के माध्यम से। PHP को CGI, शेयरमैम (या थ्रेड्स, शेयर्डम से बेहतर - और दोनों को तुरंत संकलित नहीं किया जाएगा) के लिए समर्थन के साथ बनाया जाना चाहिए;)। मैं क्या php इकट्ठा कर रहा हूँ का एक जंगली उदाहरण:





    ./configure' '--prefix=/usr/lib/php5' '--host=i686-pc-linux-gnu' '--mandir=/usr/lib/php5/man' '--infodir=/usr/lib/php5/info' '--sysconfdir=/etc' '--cache-file=./config.cache' '--disable-cli' '--enable-cgi' '--enable-fastcgi' '--disable-discard-path' '--disable-force-cgi-redirect' '--with-config-file-path=/etc/php/cgi-php5' '--with-config-file-scan-dir=/etc/php/cgi-php5/ext-active' '--without-pear' '--disable-bcmath' '--with-bz2' '--disable-calendar' '--disable-ctype' '--without-curl' '--without-curlwrappers' '--disable-dbase' '--disable-exif' '--without-fbsql' '--without-fdftk' '--disable-filter' '--disable-ftp' '--with-gettext' '--without-gmp' '--disable-hash' '--disable-ipv6' '--disable-json' '--without-kerberos' '--enable-mbstring' '--with-mcrypt' '--without-mhash' '--without-msql' '--without-mssql' '--with-ncurses' '--with-openssl' '--with-openssl-dir=/usr' '--disable-pcntl' '--without-pgsql' '--without-pspell' '--without-recode' '--disable-simplexml' '--enable-shmop' '--with-snmp' '--disable-soap' '--enable-sockets' '--without-sybase' '--without-sybase-ct' '--disable-sysvmsg' '--disable-sysvsem' '--disable-sysvshm' '--with-tidy' '--disable-tokenizer' '--disable-wddx' '--disable-xmlreader' '--disable-xmlwriter' '--without-xmlrpc' '--without-xsl' '--disable-zip' '--with-zlib' '--disable-debug' '--enable-dba' '--without-cdb' '--without-db4' '--without-flatfile' '--with-gdbm' '--without-inifile' '--without-qdbm' '--with-freetype-dir=/usr' '--with-t1lib=/usr' '--disable-gd-jis-conv' '--with-jpeg-dir=/usr' '--with-png-dir=/usr' '--without-xpm-dir' '--with-gd' '--with-ldap' '--without-ldap-sasl' '--with-mysql=/usr' '--with-mysql-sock=/var/run/mysqld/mysqld.sock' '--without-mysqli' '--without-pdo-dblib' '--with-pdo-mysql=/usr' '--without-pdo-odbc' '--without-pdo-pgsql' '--without-pdo-sqlite' '--with-readline' '--without-libedit' '--with-mm' '--without-sqlite'









    ठीक से लाइटटैप पर फास्ट करें, और किसी भी तरह से नहीं:





      fastcgi.server = (
         ".php" => (
             "लोकलहोस्ट" => (
                 <b> "सॉकेट" => "/tmp/php5-gmru-sandbox-mocksoul-lighttpd.sock" [# 1] </ b>,
                 <b> "बिन-पथ" => "/ usr / lib / php5 / bin / php-cgi -c" + "/ path / to / application / config / php_config_dir" [# 2 </ b>,
                 <b> "min-procs" => 1 [# 3] </ b>,
                 <b> "अधिकतम-समर्थक" => 1 [# 3] </ b>,
                 "बिन-पर्यावरण" => (
                     <b> "PHP_FCGI_CHILDREN" => "32" [# 4] </ b>,
                     <b> "PHP_FCGI_MAX_REQUESTS" => "3200" [# 5] </ b>
                 )
             )
         )
     ) 






    ([# 1], [# 2], ... - यह है कि मैं कोड पर टिप्पणियों का उल्लेख कैसे करूंगा। यदि आप कोड लेना चाहते हैं, तो आपको ऐसे निशान हटाने होंगे। कोड में नीचे मैं उसी योजना का पालन करूंगा)





    • [# 1] - यूनिक्स सॉकेट टीसीपी सॉकेट्स की तुलना में बहुत तेज हैं। इसलिए उन्हें केवल तभी उपयोग करें जब टीसीपी के लिए कोई गंभीर आवश्यकता नहीं है (या, विंडोज :) के तहत, हाहा)
    • [# 2] - यहाँ मैंने एक उदाहरण दिखाया कि कैसे एक अलग होस्ट को पीसीपी कॉन्फिग स्क्रू किया जाता है (इसके माध्यम से हम फोल्डर के साथ फोल्डर की ओर इशारा करते हैं)
    • [# 3] - न्यूनतम-प्रोक्स और अधिकतम-प्रोस्ट बीईटी = 1 !!! क्यों? क्योंकि आगे मैं bytecode कैशिंग के बारे में कहूंगा। 1 से अधिक पीसीपी की प्रक्रियाओं की संख्या के साथ कैश अतार्किक होगा
    • [# ४] - जादू नृत्य। हम php को lighttpd से रिक्वेस्ट प्रोसेस करने के लिए एक प्रोसेस में 32 थ्रेड चलाने के लिए कहते हैं। महत्वपूर्ण: यदि आप कहते हैं, उदाहरण के लिए, 10 और सभी 10 किसी तरह की जंगली 10-सेकंड-निष्पादित स्क्रिप्ट पर कब्जा कर लेंगे - लाइटटीडी 500 त्रुटि देगा! यानी थ्रेड्स की संख्या वास्तविक समय में नहीं बढ़ती है - 32, 64, या यहां तक ​​कि 128 डालें (यह थ्रेडपूल की तरह काम करता है)
    • [# ५] - कृपया स्ट्रीम को मारें और nth संख्या के अनुरोध के माध्यम से एक नया बनाएं। बस के मामले में, php सही नहीं है :)।


  3. ओपकोड कैचर। या एक बायोटेक कैश। या "हर अनुरोध के साथ एक ही फाइल को पार्स करने के लिए किस तरह का द्वंद्ववाद है?" मैं अत्यधिक (बहुत!) APC (वैकल्पिक PHP कैश) की सिफारिश करता हूं जो PECL में निहित है। आप eAccelerator या ZendOptimizer भी कर सकते हैं। अलग-अलग स्वाद हैं ... लेकिन eAccelerator और APC के बीच चयन करते समय - मैं APC की सलाह देता हूं। क्यों? हां, कम से कम shmem सेगमेंट में कुछ भी डालने के अवसर के लिए :)। मैं नीचे बताऊंगा।







चरण 1. हम लिखते हैं





पहले हम लिखते हैं। हम अपने सिर के विचारों में लिखते हैं और कहते हैं कि कुछ और अधिक उचित और त्वरित कैसे बनाया जाए। बाद में विचलित न होने के लिए (सामान्य तौर पर, यह शायद किसी भी स्वाभिमानी प्रोग्रामर% की पूरी तरह से स्वाभाविक इच्छा है)





क्षण जो आपको तुरंत ध्यान देने की आवश्यकता है:





  1. आप शायद आवश्यकता का उपयोग करने और शामिल करने के लिए शायद ही आवश्यकता होगी। अधिकतर आवश्यकता_नहीं और शामिल_नहीं।
  2. सरणियों पर पुनरावृत्ति करने के लिए, उन्हें बदलें और उन्हें फ़िल्टर करें - हम php में array_ * फ़ंक्शन का उपयोग करना सीखते हैं। विशेष रूप से मेमने के कार्य:

      <? php
            
     $ गिरफ्तार = सरणी ('वह', 'है', 'यह');
     array_walk ($ गिरफ्तारी, create_function ('& $ v, $ k', '$ v = $ v। "हाँ";'));
     प्रिंट_र ($ गिरफ्तारी);
    
     // आउटपुट:
     // सरणी
     // (
     // [0] => कि हाँ
     // [1] => हाँ है
     // [2] => यह हाँ
     //)
    
     // क्या आप इसे लूप बनाएंगे?  अय-आह-आह…
    
     ?> 


  3. संदर्भ द्वारा एक चर पास करना (उदाहरण के लिए $ a = 1; call_func (& $ a)) - प्रदर्शन को प्रभावित नहीं करता है। संदर्भ द्वारा सरणियों को पास करना - थोड़ा प्रभावित करता है। कक्षाओं को पास करना बहुत प्रभावशाली है। मेरा मतलब यह है कि कार्यक्रम को गति देने की उम्मीद से कुछ भी पारित न करें। लिंक पर तभी पास करें जब आपको वास्तव में इसकी आवश्यकता हो
  4. यदि संभव हो तो कक्षाओं को स्थिर बनाएं। यानी यदि, कक्षा में काम करने के लिए, एक बंद प्राधिकरण की आम तौर पर आवश्यकता नहीं है।
  5. आप जितना चाहें उतना कमेंट कर सकते हैं - बाईटेकोड कैश अभी भी टिप्पणियों की अनदेखी करता है। यह प्रदर्शन को प्रभावित करता है ... हम्म ... 0.000001% द्वारा :)
  6. गहरी पुनरावृत्ति से बचें। मानक कार्य - सब-कोड सहित फ़ाइलों की एक सूची लेने के लिए सभी पर पुनरावृत्ति के बिना किया जा सकता है =)
  7. पढ़िए साक्षर डॉक। उसी ZendFramework का दस्तावेज़ीकरण - उन लोगों के लिए भी बहुत सारी उपयोगी चीजें हैं जो फ्रेमवर्क का उपयोग नहीं करते हैं और इसका उपयोग नहीं करने जा रहे हैं
  8. कोड को तार्किक ब्लॉकों में विभाजित करने का प्रयास करें। ताकि आप एक पंक्ति में 10-20 पंक्तियाँ ले सकें और कह सकें - यहाँ मैं केवल यह कर रहा हूँ। अन्य 10-20 लें - और कहें, और यहाँ मैं केवल अन्य काम कर रहा हूँ। लाइनों की संख्या, निश्चित रूप से, आप पर निर्भर करती है। लेकिन यह बेहतर है कि ब्लॉक 30-40 लाइनों से अधिक लंबे नहीं थे। प्रोग्राम और किसी भी ब्लॉग को आरंभीकरण, कॉन्फ़िगरेशन, ऑपरेशन में तोड़ें, परिणाम को सहेजना (चलो एक चर में कहते हैं)। गति का इससे क्या लेना-देना है? छह महीने के बाद आप समझेंगे;)।
  9. के बारे में "मुझे $ a = Can कुछ $ v इनलाइन बना सकते हैं" या $ a = $ कुछ "। $ वी। "वार" सोचने लायक नहीं है। व्यक्तिगत रूप से, I (IMHO) को चर सीधे स्ट्रिंग्स में सम्मिलित करने के लिए बिल्कुल नीरस लगता है। बेहतर पठनीयता:

    • $ वर = 'कुछ'। $ में 'ली'। $ ने 'चर';
    • $ var = स्प्रिंटफ ('कुछ% sli% s चर', $ में, $ li);


  10. जो कभी नहीं बदलता है उसके लिए स्थिरांक का उपयोग करें। वे शुरुआत में ही पारंगत हो जाते हैं और आम तौर पर सामान्य चर की तुलना में स्मृति के एक अलग हिस्से में रहते हैं। $ $ = 'कुछ' फार्म का निर्माण। STR_CONSTANT और भी बेहतर दिखेंगे। विशेष रूप से सक्षम - लाइन ब्रेक। वे उसे अलग तरह से बुलाते हैं, लेकिन मुझे NL (NewLine) या CRLF (CarretReturnLineFeed) से प्यार है
  11. मत भूलो कि foreach सरणी की एक प्रति नहीं बना सकता है :)

      foreach ($ गिरफ्तारी के रूप में $ कुंजी => <b> और $ वैल </ b>) {...} 


  12. विरोधाभास जैसा कि लग सकता है, खाप में ऐसा क्षण मुझे पूरी तरह से मार देता है: is_null () - यह एक बेवकूफ द्वारा आविष्कार किया गया था। if (null === $ var) या if ($ var === null) अगर (is_null ($ var)) ... dibilism की तुलना में तेज़ है। Is_null () का उपयोग न करें :)
  13. नियमित अभिव्यक्तियाँ, str_ * फ़ंक्शंस का उपयोग करते हुए स्ट्रिंग्स के साथ काम करना, मैं आपकी अंतरात्मा पर छोड़ता हूं क्योंकि यह पहले से ही लिखित शब्द के दायरे से परे है :)


स्टेज 2. हम समय की संभावित बर्बादी के बारे में सोचते हैं।





तो ... यहाँ आपने कुछ लिखा है। अब देखते हैं कि आमतौर पर आपके व्यावसायिक तर्क के बिना बहुत सारे डॉफिग होते हैं:



  1. DB से कनेक्ट करें
  2. हैंडलिंग टन की आवश्यकता होती है
  3. डीबी ने खुद से पूछताछ की
  4. क्या हम कॉन्फिगरेशन को कहीं स्टोर करते हैं और इसे हर बार पार्स करते हैं? हम डेटाबेस मॉडल का उपयोग करते हैं और हर बार उन्हें इनिशियलाइज़ करते हैं? आम तौर पर देखो हम एक ही अनुरोध करते हैं!
  5. फ़ाइल सिस्टम के साथ कुछ करना? और क्यों? व्यक्तिगत रूप से, मुझे लगता है कि आप लगभग किसी भी परियोजना को बिना IO के साथ लिख सकते हैं (ज़ाहिर है, सिवाय इसके कि यह डेटाबेस का उपयोग करेगा, आदि)। फ़ाइल सिस्टम पर कुछ भी संग्रहीत करने की आवश्यकता नहीं है। पैटी। बड़ी (कुछ गिग अनुक्रमित फ़ाइल) - आवश्यकता




मैंने इसे सभी को महत्व दिया। और अब, प्रत्येक अप्राप्य क्षण के लिए:



DB से कनेक्ट करें



यह सरल है - यदि आप एक सर्वर के मालिक हैं - लगातार कनेक्शन का उपयोग करें! PDO_MYSQL, MYSQL - वे सभी जानते हैं कि कैसे)



हैंडलिंग टन की आवश्यकता होती है



यह वह जगह है जहाँ मज़ा शुरू होता है =)। आरंभ करने के लिए, मैंने ZendFramework में किसी भी अनुरोध के साथ कितनी फ़ाइलों को शामिल किया है, इस पर एक नज़र डाली। यह पता चला - 300 से थोड़ा कम (!!!!)। यदि आप एक बाइटकोड कैश का उपयोग नहीं करते हैं, तो यह एक असामान्य रूप से लंबी प्रक्रिया होगी।



समाधान "लॉक" खुद से पाया गया - यह सब एक फ़ाइल में रटना। सवाल उठता है - और कैसे पता करें कि हमेशा हमारे साथ क्या होता है - और क्या कभी-कभी? सामान्य तौर पर, उस समय सोचने के लिए ज्यादा समय नहीं था, इसलिए मैंने इस पहलू का फैसला किया "शिथिल")



जंगली परिणाम - http://www.mocksoul.ru/pub/dev/mkzend.phps



वहाँ:





स्क्रिप्ट पूरी तरह से अस्थिर है और एक परियोजना के लिए कैद है। आपको एपीसी के लिए ब्राउज़र के माध्यम से काम करना होगा। उदाहरण के तौर पर। आप 100% संभावना =) के साथ काम नहीं करेंगे।



जैसा कि यह निकला, 300 फाइलें 2 सेकंड के लिए पार्स की गईं, उन्हें 0.3 सेकंड में बाइट कैश से निकाला गया, और उत्पन्न सुपरफाइल को एक बड़े 0.7sec के लिए पार्स किया गया और 0.003sec में कैश से बाहर निकाला गया। परियोजना ने लगभग 3 बार त्वरित रूप से त्वरित किया :)। उन्मत्त अनुकूलन, हालांकि। विधि एक उत्पादन सर्वर के लिए उपयुक्त है, जैसा कि किसी अन्य फ़ाइल से लोड किए गए पुस्तकालयों को विकसित करना असंभव है।



डेटाबेस क्वेरी



DBA का भ्रमण करें और अंत में MYSQL_QUERY_CACHE का उपयोग करना शुरू करें। My.cnf में, query_cache_size = 100M



लिखें। हम कैश को show status like 'qcache%'



फॉलो करते हैं। अभी भी बहुत कसकर पढ़ा MySQL डॉक्स क्वेरी कैश के बारे में





एक ही काम करना बंद करो - कैश!





क्या आपने कॉन्फिग पढ़ा है? क्या आप भाप ले रहे हैं? एक तैयार किया सरणी है? खैर, इसे फिर से क्यों? ) आपके पास है - एपीसी के रूप में हाथ में साझा की गई स्मृति! :) काम की अविश्वसनीय रूप से तेज गति ... इसमें जो कुछ भी आप कर सकते हैं उसे स्टोर करें - कॉन्फ़िगरेशन, एकत्र की गई वस्तुएं, क्वेरी का परिणाम एक ला "विवरण तालिका" है (यह Zend_Db_Table_ * का विशेषाधिकार है)। डेटा कैश से अकल्पनीय गति से लिया गया है - 0.000001 कहीं। मेमोरी में, यदि आप कुछ भी डुप्लिकेट नहीं करते हैं, तो आप सिर्फ डॉफीगा डेटा बचा सकते हैं। याद रखें कि 1 टमटम संभव जानकारी का एक विशाल गुच्छा है। इसके लिए फ़ाइल सिस्टम में IO का उपयोग न करें - बेहतर मेमोरी। आपकी योग्यता के आधार पर - गति में 10 से 100% की वृद्धि। एपीडी के बारे में नीचे देखें;)



आपको FS की आवश्यकता क्यों है?





FS का उपयोग किसी भी चीज़ के रक्षक के रूप में करें यदि वह मेमोरी में फिट नहीं होता है। यहां तक ​​कि अगर आप एक लॉग या क्वेरी आँकड़े लिखते हैं - एपीसी पर जाएं! और बचाओ, कहते हैं, पेंच पर हर 5 मिनट में।

चरण 3. समय बर्बाद करने के बारे में सोचकर थक गए। हम अपनी आँखों के सामने एक कार्यक्रम चाहते हैं!





यह मेरे लिए बहुत मूल्यवान खोज थी। सामान्य तौर पर, चरण-दर-चरण मार्गदर्शिका:



  1. हमें PECL APD (उन्नत PHP डीबगर) की आवश्यकता है
  2. विन्यास में एपीडी के लिए डंपडिर को कॉन्फ़िगर करना। कुछ इस तरह:

      zend_extension = / usr / lib / php5 / lib / php / extension / no-debug-non-zts-20060613 / apd.so
     apd.dumpdir = "/ tmp / php-apd- डंप" 
  3. सबसे महत्वपूर्ण फ़ाइल में, फ़ाइल के शीर्ष पर apd_set_pprof_trace();



    लिखें apd_set_pprof_trace();



    , जिसमें एक प्रोफाइल डंप भी शामिल है
  4. हम सर्वर से 1-100 अनुरोध करते हैं। हर बार एक नई फ़ाइल हमारे / tmp / php-apd- डंप में सहेजी जाएगी
  5. अब हम प्रोफाइलर के परिणामों को या तो सीधे कंसोल में देख सकते हैं - साथ में एपिड pprofp स्क्रिप्ट आता है
  6. और हम एक सुपर काम कर सकते हैं - इसे और अधिक एकीकृत प्रारूप में कनवर्ट करें :)। APD के साथ, pprofp के अलावा, pprof2calltree भी है। यह विशेष रूप से कैशग्रिंड और केचगग्रिंड द्वारा समझे गए प्रारूप में प्रोफाइलर डंप को धर्मान्तरित करता है। परिणामी फ़ाइल kcachegrind में खोली गई है - और खुशी के साथ तालियाँ।





सामान्य तौर पर - इस तरह के एक साधारण प्रोफाइलर प्राप्त किया जाता है। यह सिर्फ PHP के लिए है, मैंने ऐसा पहले नहीं किया है;)



चरण 4. जाँच





ab



या ab2



का उपयोग करके 1 URL के लिए सरल प्रश्नों के साथ गति की जाँच करना बेवकूफी है।



एक और अधिक तार्किक विकल्प सभी (या सभी नहीं;) की सूची बनाने के लिए है, एक पाठ फ़ाइल में डाला जाता है, घेराबंदी और परीक्षण करें। परीक्षण के दौरान, शिकंजा के लिए TPS (TransactionsPerSecond) (उदाहरण के लिए, sysstat पैकेज से iostat का उपयोग करके) की निगरानी करें, प्रोसेसर लोड की निगरानी करें, देखें कि अंत में 2xx के अलावा कोई सर्वर प्रतिक्रियाएं नहीं हैं।



वह सब क्यों है?



चीजों को गति देने की कोशिश करने के लिए बहुत कुछ है जब परियोजना बढ़ती है। 1 सर्वर पर 10% की गति से वृद्धि 10% के बराबर गति में वृद्धि देती है। और अगर आपके पास पहले से ही 10 सर्वर हैं, तो गति में 10% की बढ़ोतरी दूसरे 11 वें सर्वर को जोड़ने के बराबर होगी। यानी 1 सर्वर के संदर्भ में + 100%। यह बहुत कुछ है। यह पैसा है। और यह प्रतियोगियों के लिए एक उच्च प्रवेश सीमा है;)।



eeee



2 दिन पहले मैंने अपना कॉलरबोन तोड़ दिया। और उसने यह सब एक हाथ से लिखा था। मेरे लिए स्मारक !!! :))



तरह का संबंध है, वादिम बर्माकिन उर्फ ​​मॉकसॉल © 2007



All Articles