கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்

பெரும்பாலும், சேவை அளவீடுகளை ஏன் சேகரிக்க வேண்டும் என்று இன்று யாரும் கேட்கவில்லை. அடுத்த தர்க்கரீதியான படி, சேகரிக்கப்பட்ட அளவீடுகளுக்கான விழிப்பூட்டலை அமைப்பதாகும், இது உங்களுக்கு வசதியான சேனல்களில் (அஞ்சல், ஸ்லாக், டெலிகிராம்) தரவுகளில் ஏதேனும் விலகல்கள் குறித்து தெரிவிக்கும். ஆன்லைன் ஹோட்டல் முன்பதிவு சேவையில் Ostrovok.ru எங்கள் சேவைகளின் அனைத்து அளவீடுகளும் InfluxDB இல் ஊற்றப்பட்டு கிராஃபானாவில் காட்டப்படும், மேலும் அடிப்படை விழிப்பூட்டலும் அங்கு கட்டமைக்கப்பட்டுள்ளது. "நீங்கள் எதையாவது கணக்கிட்டு அதனுடன் ஒப்பிட வேண்டும்" போன்ற பணிகளுக்கு நாங்கள் கபாசிட்டரைப் பயன்படுத்துகிறோம்.

கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்
கபாசிட்டர் என்பது டிக் ஸ்டேக்கின் ஒரு பகுதியாகும், இது InfluxDB இலிருந்து அளவீடுகளை செயலாக்க முடியும். இது பல அளவீடுகளை ஒன்றாக இணைக்கலாம் (சேரலாம்), பெறப்பட்ட தரவிலிருந்து பயனுள்ள ஒன்றைக் கணக்கிடலாம், முடிவை மீண்டும் InfluxDB க்கு எழுதலாம், Slack/Telegram/mail க்கு எச்சரிக்கையை அனுப்பலாம்.

முழு அடுக்கும் குளிர்ச்சியாகவும் விரிவாகவும் உள்ளது ஆவணங்கள், ஆனால் கையேடுகளில் வெளிப்படையாகக் குறிப்பிடப்படாத பயனுள்ள விஷயங்கள் எப்போதும் இருக்கும். இந்த கட்டுரையில், இதுபோன்ற பல பயனுள்ள, வெளிப்படையான உதவிக்குறிப்புகளை சேகரிக்க முடிவு செய்தேன் (TICKscipt இன் அடிப்படை தொடரியல் விவரிக்கப்பட்டுள்ளது இங்கே) மற்றும் எங்கள் பிரச்சனைகளில் ஒன்றைத் தீர்ப்பதற்கான உதாரணத்தைப் பயன்படுத்தி அவற்றை எவ்வாறு பயன்படுத்தலாம் என்பதைக் காட்டவும்.

போகலாம்!

float & int, கணக்கீடு பிழைகள்

சாதிகள் மூலம் தீர்க்கப்படும் முற்றிலும் நிலையான பிரச்சனை:

var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))

இயல்புநிலையைப் பயன்படுத்துதல்()

குறிச்சொல்/புலம் நிரப்பப்படாவிட்டால், கணக்கீடு பிழைகள் ஏற்படும்:

|default()
        .tag('status', 'empty')
        .field('value', 0)

நிரப்பவும் (உள் vs வெளி)

இயல்பாக, இணைப்பானது தரவு இல்லாத (உள்) புள்ளிகளை நிராகரிக்கும்.
நிரப்பு('null') உடன், ஒரு வெளிப்புற இணைப்பு செய்யப்படும், அதன் பிறகு நீங்கள் இயல்புநிலை() செய்து வெற்று மதிப்புகளை நிரப்ப வேண்டும்:

var data = res1
    |join(res2)
        .as('res1', 'res2)
        .fill('null')
    |default()
        .field('res1.value', 0.0)
        .field('res2.value', 100.0)

இங்கே இன்னும் ஒரு நுணுக்கம் உள்ளது. மேலே உள்ள எடுத்துக்காட்டில், தொடரில் ஒன்று (res1 அல்லது res2) காலியாக இருந்தால், அதன் விளைவாக வரும் தொடரும் (தரவு) காலியாக இருக்கும். Github இல் இந்த தலைப்பில் பல டிக்கெட்டுகள் உள்ளன (1633, 1871, 6967) - நாங்கள் திருத்தங்களுக்காக காத்திருக்கிறோம் மற்றும் கொஞ்சம் கஷ்டப்படுகிறோம்.

கணக்கீடுகளில் நிபந்தனைகளைப் பயன்படுத்துதல் (லாம்ப்டாவில் இருந்தால்)

|eval(lambda: if("value" > 0, true, false)

காலத்திற்கான பைப்லைனிலிருந்து கடைசி ஐந்து நிமிடங்கள்

எடுத்துக்காட்டாக, கடந்த ஐந்து நிமிடங்களின் மதிப்புகளை முந்தைய வாரத்துடன் ஒப்பிட வேண்டும். நீங்கள் இரண்டு தனித்தனி பேட்ச்களில் இரண்டு பேட்ச் தரவை எடுக்கலாம் அல்லது ஒரு பெரிய காலகட்டத்திலிருந்து தரவின் ஒரு பகுதியைப் பிரித்தெடுக்கலாம்:

 |where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)

கடைசி ஐந்து நிமிடங்களுக்கு மாற்றாக, குறிப்பிட்ட நேரத்திற்கு முன்பே தரவைத் துண்டிக்கும் BarrierNode ஐப் பயன்படுத்துவது:

|barrier()
        .period(5m)

செய்தியில் Go டெம்ப்ளேட்களைப் பயன்படுத்துவதற்கான எடுத்துக்காட்டுகள்

வார்ப்புருக்கள் தொகுப்பின் வடிவமைப்பிற்கு ஒத்திருக்கும் text.templateஅடிக்கடி சந்திக்கும் சில புதிர்கள் கீழே உள்ளன.

இல்லையென்றால்

நாங்கள் விஷயங்களை ஒழுங்கமைக்கிறோம், மேலும் உரையுடன் மக்களை மீண்டும் தூண்ட வேண்டாம்:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
    )

செய்தியில் தசம புள்ளிக்குப் பிறகு இரண்டு இலக்கங்கள்

செய்தியின் வாசிப்புத் திறனை மேம்படுத்துதல்:

|alert()
    ...
    .message(
        'now value is {{ index .Fields "value" | printf "%0.2f" }}'
    )

செய்தியில் மாறிகளை விரிவுபடுத்துகிறது

"ஏன் கத்துகிறது" என்ற கேள்விக்கு பதிலளிக்க, செய்தியில் கூடுதல் தகவலைக் காண்பிக்கிறோம்?

var warnAlert = 10
  |alert()
    ...
    .message(
       'Today value less then '+string(warnAlert)+'%'
    )

தனித்துவமான எச்சரிக்கை அடையாளங்காட்டி

தரவுகளில் ஒன்றுக்கு மேற்பட்ட குழுக்கள் இருக்கும்போது இது அவசியமான விஷயம், இல்லையெனில் ஒரே ஒரு எச்சரிக்கை உருவாக்கப்படும்:

|alert()
      ...
      .id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')

தனிப்பயன் கையாளுபவர்கள்

கையாளுபவர்களின் பெரிய பட்டியலில் exec அடங்கும், இது உங்கள் ஸ்கிரிப்டை அனுப்பிய அளவுருக்களுடன் (stdin) செயல்படுத்த அனுமதிக்கிறது - படைப்பாற்றல் மற்றும் அதற்கு மேல் எதுவும் இல்லை!

ஸ்லாக்கிற்கு அறிவிப்புகளை அனுப்புவதற்கான சிறிய பைதான் ஸ்கிரிப்ட் எங்கள் பழக்கவழக்கங்களில் ஒன்றாகும்.
முதலில், அங்கீகாரம்-பாதுகாக்கப்பட்ட கிராஃபானா படத்தை ஒரு செய்தியில் அனுப்ப விரும்பினோம். பின்னர், அதே குழுவில் இருந்து முந்தைய எச்சரிக்கைக்கு திரியில் சரி என்று எழுதவும், தனி செய்தியாக அல்ல. சிறிது நேரம் கழித்து - கடைசி X நிமிடங்களில் மிகவும் பொதுவான தவறை செய்தியில் சேர்க்கவும்.

ஒரு தனித் தலைப்பு பிற சேவைகளுடனான தொடர்பு மற்றும் விழிப்பூட்டல் மூலம் தொடங்கப்படும் எந்த செயல்களும் (உங்கள் கண்காணிப்பு போதுமான அளவு செயல்பட்டால் மட்டுமே).
ஹேண்ட்லர் விளக்கத்தின் உதாரணம், slack_handler.py என்பது எங்களின் சுயமாக எழுதப்பட்ட ஸ்கிரிப்ட்:

topic: slack_graph
id: slack_graph.alert
match: level() != INFO AND changed() == TRUE
kind: exec
options:
  prog: /sbin/slack_handler.py
  args: ["-c", "CHANNELID", "--graph", "--search"]

பிழைத்திருத்தம் செய்வது எப்படி?

பதிவு வெளியீட்டைக் கொண்ட விருப்பம்

|log()
      .level("error")
      .prefix("something")

பார்க்கவும் (cli): கபாசிட்டர் -url ஹோஸ்ட்-அல்லது-ஐபி:9092 பதிவுகள் lvl=பிழை

httpOut உடன் விருப்பம்

தற்போதைய பைப்லைனில் தரவைக் காட்டுகிறது:

|httpOut('something')

பார்க்கவும் (பெறவும்): ஹோஸ்ட்-அல்லது-ஐபி:9092/kapacitor/v1/tasks/task_name/something

செயல்படுத்தும் திட்டம்

  • ஒவ்வொரு பணியும் வடிவமைப்பில் பயனுள்ள எண்களைக் கொண்ட ஒரு செயல்பாட்டு மரத்தை வழங்குகிறது வரைபடம்.
  • ஒரு தொகுதியை எடுத்துக் கொள்ளுங்கள் டாட்.
  • பார்வையாளரில் ஒட்டவும் அனுபவிக்க.

வேறு எங்கு ரேக் கிடைக்கும்?

ரைட்பேக்கில் influxdb இல் நேர முத்திரை

எடுத்துக்காட்டாக, ஒரு மணிநேரத்திற்கான கோரிக்கைகளின் தொகைக்கு (groupBy(1h)) விழிப்பூட்டலை அமைத்து, influxdb இல் ஏற்பட்ட விழிப்பூட்டலைப் பதிவுசெய்ய விரும்புகிறோம் (கிராஃபனாவில் உள்ள வரைபடத்தில் சிக்கலின் உண்மையை அழகாகக் காட்ட).

influxDBOut() ஆனது விழிப்பூட்டலில் இருந்து நேர முத்திரைக்கு நேர மதிப்பை எழுதும்; அதன்படி, விளக்கப்படத்தில் உள்ள புள்ளி எச்சரிக்கை வந்ததை விட முன்னதாக/பின்னர் எழுதப்படும்.

துல்லியம் தேவைப்படும்போது: தனிப்பயன் ஹேண்ட்லரை அழைப்பதன் மூலம் இந்த சிக்கலைச் சரிசெய்வோம், இது தற்போதைய நேர முத்திரையுடன் influxdb க்கு தரவை எழுதும்.

டோக்கர், உருவாக்க மற்றும் வரிசைப்படுத்தல்

தொடக்கத்தில், கபாசிட்டர் [load] தொகுதியில் உள்ள கட்டமைப்பில் குறிப்பிடப்பட்டுள்ள கோப்பகத்தில் இருந்து பணிகள், வார்ப்புருக்கள் மற்றும் கையாளுபவர்களை ஏற்ற முடியும்.

ஒரு பணியை சரியாக உருவாக்க, உங்களுக்கு பின்வரும் விஷயங்கள் தேவை:

  1. கோப்பு பெயர் - ஸ்கிரிப்ட் ஐடி/பெயராக விரிவாக்கப்பட்டது
  2. வகை - ஸ்ட்ரீம் / தொகுதி
  3. dbrp – ஸ்கிரிப்ட் எந்த தரவுத்தளம் + கொள்கையில் இயங்குகிறது என்பதைக் குறிக்கும் முக்கிய சொல் (dbrp “சப்ளையர்.” “ஆட்டோஜென்”)

சில தொகுதி பணிகளில் dbrp உடன் வரி இல்லை என்றால், முழு சேவையும் தொடங்க மறுக்கும் மற்றும் நேர்மையாக பதிவில் அதைப் பற்றி எழுதும்.

க்ரோனோகிராஃபில், மாறாக, இந்த வரி இருக்கக்கூடாது; இது இடைமுகம் மூலம் ஏற்றுக்கொள்ளப்படவில்லை மற்றும் பிழையை உருவாக்குகிறது.

ஒரு கொள்கலனை உருவாக்கும்போது ஹேக்: //.+dbrp உடன் கோடுகள் இருந்தால் -1 உடன் Dockerfile வெளியேறுகிறது, இது கட்டமைப்பை அசெம்பிள் செய்யும் போது தோல்விக்கான காரணத்தை உடனடியாகப் புரிந்துகொள்ள உங்களை அனுமதிக்கும்.

ஒன்று பல சேர

எடுத்துக்காட்டு பணி: சேவையின் இயக்க நேரத்தின் 95 வது சதவீதத்தை நீங்கள் ஒரு வாரத்திற்கு எடுக்க வேண்டும், கடைசி 10 நிமிடங்களின் ஒவ்வொரு நிமிடத்தையும் இந்த மதிப்புடன் ஒப்பிடவும்.

நீங்கள் ஒன்றுக்கு பல சேர முடியாது, கடைசி/சராசரி/நடுநிலை புள்ளிகளின் குழுவில் முனையை ஸ்ட்ரீமாக மாற்றினால், "குழந்தை பொருந்தாத விளிம்புகளைச் சேர்க்க முடியாது: தொகுதி -> ஸ்ட்ரீம்" என்ற பிழை திரும்பும்.

லாம்ப்டா வெளிப்பாட்டின் மாறியாக ஒரு தொகுதியின் முடிவும் மாற்றாக இல்லை.

udf வழியாக ஒரு கோப்பில் முதல் தொகுப்பிலிருந்து தேவையான எண்களைச் சேமித்து, சைட்லோட் வழியாக இந்தக் கோப்பை ஏற்ற ஒரு விருப்பம் உள்ளது.

இதற்கு என்ன தீர்வு கண்டோம்?

எங்களிடம் சுமார் 100 ஹோட்டல் சப்ளையர்கள் உள்ளனர், அவர்கள் ஒவ்வொருவருக்கும் பல இணைப்புகள் இருக்கலாம், அதை ஒரு சேனல் என்று அழைக்கலாம். இந்த சேனல்களில் தோராயமாக 300 உள்ளன, ஒவ்வொரு சேனல்களும் செயலிழக்கக்கூடும். பதிவுசெய்யப்பட்ட அனைத்து அளவீடுகளிலும், பிழை விகிதத்தை (கோரிக்கைகள் மற்றும் பிழைகள்) கண்காணிப்போம்.

ஏன் கிராஃபானா இல்லை?

கிராஃபானாவில் உள்ளமைக்கப்பட்ட பிழை விழிப்பூட்டல்கள் பல குறைபாடுகளைக் கொண்டுள்ளன. சில முக்கியமானவை, சில சூழ்நிலையைப் பொறுத்து கண்களை மூடிக்கொள்ளலாம்.

அளவீடுகள் + விழிப்பூட்டல்களுக்கு இடையே எப்படி கணக்கிடுவது என்று கிராஃபனாவுக்குத் தெரியாது, ஆனால் எங்களுக்கு ஒரு விகிதம் (கோரிக்கைகள்-பிழைகள்)/கோரிக்கைகள் தேவை.

பிழைகள் மோசமானவை:

கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்

வெற்றிகரமான கோரிக்கைகளுடன் பார்க்கும்போது குறைவான தீமை:

கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்

சரி, கிராஃபனாவிற்கு முன் சேவையின் கட்டணத்தை முன்கூட்டியே கணக்கிடலாம், சில சமயங்களில் இது வேலை செய்யும். ஆனால் நம்மில் இல்லை, ஏனென்றால்... ஒவ்வொரு சேனலுக்கும் அதன் சொந்த விகிதம் "சாதாரணமானது" என்று கருதப்படுகிறது, மேலும் விழிப்பூட்டல்கள் நிலையான மதிப்புகளின்படி செயல்படுகின்றன (நாங்கள் அவற்றை எங்கள் கண்களால் தேடுகிறோம், அடிக்கடி எச்சரிக்கைகள் இருந்தால் அவற்றை மாற்றவும்).

இவை வெவ்வேறு சேனல்களுக்கான "சாதாரண" எடுத்துக்காட்டுகள்:

கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்

கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்

முந்தைய புள்ளியை நாங்கள் புறக்கணிக்கிறோம் மற்றும் "சாதாரண" படம் அனைத்து சப்ளையர்களுக்கும் ஒத்ததாக இருக்கும் என்று கருதுகிறோம். இப்போது எல்லாம் சரியாகிவிட்டது, கிராஃபனாவில் விழிப்பூட்டல்களைப் பெற முடியுமா?
எங்களால் முடியும், ஆனால் நாங்கள் உண்மையில் விரும்பவில்லை, ஏனென்றால் நாங்கள் விருப்பங்களில் ஒன்றைத் தேர்ந்தெடுக்க வேண்டும்:
அ) ஒவ்வொரு சேனலுக்கும் தனித்தனியாக நிறைய வரைபடங்களை உருவாக்கவும் (மற்றும் வலிமிகுந்த வகையில் அவற்றுடன்)
b) அனைத்து சேனல்களிலும் ஒரு விளக்கப்படத்தை விட்டு விடுங்கள் (மற்றும் வண்ணமயமான கோடுகள் மற்றும் தனிப்பயனாக்கப்பட்ட விழிப்பூட்டல்களில் தொலைந்து போங்கள்)

கபாசிட்டரில் அளவீடுகளைச் செயலாக்குவதற்கான தந்திரங்கள்

நீங்கள் அதை எப்படி செய்தீர்கள்?

மீண்டும், ஆவணத்தில் ஒரு நல்ல தொடக்க உதாரணம் உள்ளது (இணைந்த தொடர் முழுவதும் கட்டணங்களைக் கணக்கிடுகிறது), இதே போன்ற பிரச்சனைகளில் எட்டிப்பார்க்கலாம் அல்லது அடிப்படையாக எடுத்துக்கொள்ளலாம்.

இறுதியில் என்ன செய்தோம்:

  • சில மணிநேரங்களில் இரண்டு தொடர்களில் சேரவும், சேனல்கள் மூலம் குழுவாகவும்;
  • தரவு இல்லை என்றால் குழு மூலம் தொடரை நிரப்பவும்;
  • முந்தைய தரவுகளுடன் கடந்த 10 நிமிடங்களின் சராசரியை ஒப்பிடுக;
  • ஏதாவது கிடைத்தால் கத்துவோம்;
  • influxdb இல் ஏற்பட்ட கணக்கிடப்பட்ட விகிதங்கள் மற்றும் விழிப்பூட்டல்களை நாங்கள் எழுதுகிறோம்;
  • ஸ்லாக்கிற்கு பயனுள்ள செய்தியை அனுப்பவும்.

என் கருத்துப்படி, இறுதியில் நாங்கள் பெற விரும்பிய அனைத்தையும் (மற்றும் தனிப்பயன் கையாளுபவர்களுடன் இன்னும் கொஞ்சம் கூட) முடிந்தவரை அழகாக அடைய முடிந்தது.

நீங்கள் github.com இல் பார்க்கலாம் குறியீடு உதாரணம் и குறைந்தபட்ச சுற்று (கிராஃப்விஸ்) இதன் விளைவாக வரும் ஸ்கிரிப்ட்.

இதன் விளைவாக வரும் குறியீட்டின் எடுத்துக்காட்டு:

dbrp "supplier"."autogen"
var name = 'requests.rate'
var grafana_dash = 'pczpmYZWU/mydashboard'
var grafana_panel = '26'
var period = 8h
var todayPeriod = 10m
var every = 1m
var warnAlert = 15
var warnReset = 5
var reqQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."requests"'
var errQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."errors"'

var prevErr = batch
    |query(errQuery)
        .period(period)
        .every(every)
        .groupBy(1m, 'channel', 'supplier')

var prevReq = batch
    |query(reqQuery)
        .period(period)
        .every(every)
        .groupBy(1m, 'channel', 'supplier')

var rates = prevReq
    |join(prevErr)
        .as('req', 'err')
        .tolerance(1m)
        .fill('null')
    // заполняем значения нулями, если их не было
    |default()
        .field('err.value', 0.0)
        .field('req.value', 0.0)
    // if в lambda: считаем рейт, только если ошибки были
    |eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
        .as('rate')

// записываем посчитанные значения в инфлюкс
rates
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('rates')

// выбираем данные за последние 10 минут, считаем медиану
var todayRate = rates
    |where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) < todayPeriod)
    |median('rate')
        .as('median')

var prevRate = rates
    |median('rate')
        .as('median')

var joined = todayRate
    |join(prevRate)
        .as('today', 'prev')
    |httpOut('join')

var trigger = joined
    |alert()
        .warn(lambda: ("prev.median" - "today.median") > warnAlert)
        .warnReset(lambda: ("prev.median" - "today.median") < warnReset)
        .flapping(0.25, 0.5)
        .stateChangesOnly()
        // собираем в message ссылку на график дашборда графаны
        .message(
            '{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}It is ok now{{ else }}
'+string(todayPeriod)+' median is {{ index .Fields "today.median" | printf "%0.2f" }}%, by previous '+string(period)+' is {{ index .Fields "prev.median" | printf "%0.2f" }}%{{ end }}
http://grafana.ostrovok.in/d/'+string(grafana_dash)+
'?var-supplier={{ index .Tags "supplier" }}&var-channel={{ index .Tags "channel" }}&panelId='+string(grafana_panel)+'&fullscreen&tz=UTC%2B03%3A00'
        )
        .id('{{ index .Tags "name" }}/{{ index .Tags "channel" }}')
        .levelTag('level')
        .messageField('message')
        .durationField('duration')
        .topic('slack_graph')

// "today.median" дублируем как "value", также пишем в инфлюкс остальные филды алерта (keep)
trigger
    |eval(lambda: "today.median")
        .as('value')
        .keep()
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('alerts')
        .tag('alertName', name)

முடிவு என்ன?

பல குழுக்களுடன் கண்காணிப்பு-விழிப்பூட்டல்களைச் செய்வதிலும், ஏற்கனவே பதிவுசெய்யப்பட்ட அளவீடுகளின் அடிப்படையில் கூடுதல் கணக்கீடுகளைச் செய்வதிலும், தனிப்பயன் செயல்களைச் செய்வதிலும் (udf) இயங்கும் ஸ்கிரிப்ட்களிலும் கபாசிட்டர் சிறந்து விளங்குகிறது.

நுழைவதற்கான தடை மிக அதிகமாக இல்லை - கிராஃபானா அல்லது பிற கருவிகள் உங்கள் ஆசைகளை முழுமையாக பூர்த்தி செய்யவில்லை என்றால் அதை முயற்சிக்கவும்.

ஆதாரம்: www.habr.com

கருத்தைச் சேர்